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.0.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, pos, 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::WallJump
527 | GameInput::Roll
528 | GameInput::Sneak
529 | GameInput::AutoWalk
530 | GameInput::SwimUp
531 | GameInput::SwimDown
532 | GameInput::SwapLoadout
533 | GameInput::ToggleWield
534 | GameInput::Slot1
535 | GameInput::Slot2
536 | GameInput::Slot3
537 | GameInput::Slot4
538 | GameInput::Slot5
539 | GameInput::Slot6
540 | GameInput::Slot7
541 | GameInput::Slot8
542 | GameInput::Slot9
543 | GameInput::Slot10
544 | GameInput::SpectateViewpoint
545 | GameInput::SpectateSpeedBoost => return true,
546 _ => (),
547 }
548 }
549 false
550 }
551}
552
553impl PlayState for SessionState {
554 fn enter(&mut self, global_state: &mut GlobalState, _: Direction) {
555 global_state.window.grab_cursor(true);
557
558 self.client.borrow_mut().clear_terrain();
559
560 if global_state.settings.send_logon_commands {
562 for cmd in &global_state.settings.logon_commands {
563 self.client.borrow_mut().send_chat(cmd.to_string());
564 }
565 }
566
567 #[cfg(feature = "discord")]
568 {
569 #[cfg(feature = "singleplayer")]
571 let singleplayer = global_state.singleplayer.is_running();
572 #[cfg(not(feature = "singleplayer"))]
573 let singleplayer = false;
574
575 if singleplayer {
576 global_state.discord.join_singleplayer();
577 } else {
578 global_state
579 .discord
580 .join_server(self.client.borrow().server_info().name.clone());
581 }
582 }
583 }
584
585 fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
586 span!(_guard, "tick", "<Session as PlayState>::tick");
587 let (client_presence, client_type, client_registered) = {
590 let client = self.client.borrow();
591 (
592 client.presence(),
593 *client.client_type(),
594 client.registered(),
595 )
596 };
597
598 if let Some(presence) = client_presence {
599 let camera = self.scene.camera_mut();
600
601 if self.camera_clamp {
603 let mut cam_dir = camera.get_orientation();
604 let cam_dir_clamp =
605 (global_state.settings.gameplay.camera_clamp_angle as f32).to_radians();
606 cam_dir.y = cam_dir.y.clamp(-cam_dir_clamp, cam_dir_clamp);
607 camera.set_orientation(cam_dir);
608 }
609
610 let client = self.client.borrow();
611 let player_entity = client.entity();
612
613 let dt = global_state.clock.get_stable_dt().as_secs_f32();
614
615 #[cfg(feature = "discord")]
616 if global_state.discord.is_active()
617 && let Some(chunk) = client.current_chunk()
618 && let Some(location_name) = chunk.meta().name()
619 {
620 global_state
621 .discord
622 .update_location(location_name, client.current_site());
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 && cr.charge_frac() > 0.5
632 {
633 fov_scaling -= 3.0 * cr.charge_frac() / 5.0;
634 }
635 camera.set_fixate(fov_scaling);
636 } else {
637 camera.set_fixate(1.0);
638 }
639
640 camera.compute_dependents(&client.state().terrain());
642 let camera::Dependents {
643 cam_pos, cam_dir, ..
644 } = self.scene.camera().dependents();
645 let focus_pos = self.scene.camera().get_focus_pos();
646 let focus_off = focus_pos.map(|e| e.trunc());
647 let cam_pos = cam_pos + focus_off;
648
649 let (is_aiming, aim_dir_offset) = {
650 let is_aiming = client
651 .state()
652 .read_storage::<comp::CharacterState>()
653 .get(player_entity)
654 .map(|cs| cs.is_wield())
655 .unwrap_or(false);
656
657 (
658 is_aiming,
659 if is_aiming && self.scene.camera().get_mode() == CameraMode::ThirdPerson {
660 Vec3::unit_z() * 0.025
661 } else {
662 Vec3::zero()
663 },
664 )
665 };
666 self.is_aiming = is_aiming;
667
668 let can_build = client
669 .state()
670 .read_storage::<comp::CanBuild>()
671 .get(player_entity)
672 .map_or_else(|| false, |cb| cb.enabled);
673
674 let active_mine_tool: Option<ToolKind> = if client.is_wielding() == Some(true) {
675 client
676 .inventories()
677 .get(player_entity)
678 .and_then(|inv| inv.equipped(EquipSlot::ActiveMainhand))
679 .and_then(|item| item.tool_info())
680 .filter(|tool_kind| matches!(tool_kind, ToolKind::Pick | ToolKind::Shovel))
681 } else {
682 None
683 };
684
685 let (build_target, collect_target, entity_target, mine_target, terrain_target) =
687 targets_under_cursor(
688 &client,
689 cam_pos,
690 cam_dir,
691 can_build,
692 active_mine_tool,
693 self.viewpoint_entity().0,
694 );
695
696 match get_interactables(
697 &client,
698 collect_target,
699 entity_target,
700 mine_target,
701 terrain_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 && 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 {
874 self.selected_block = block;
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::WallJump => {
911 self.walking_speed = false;
912 self.client.borrow_mut().handle_input(
913 InputKind::WallJump,
914 state,
915 None,
916 self.target_entity,
917 );
918 },
919 GameInput::SwimUp => {
920 self.key_state.swim_up = state;
921 },
922 GameInput::SwimDown => {
923 self.key_state.swim_down = state;
924 },
925 GameInput::Sit => {
926 if state && controlling_char {
927 self.stop_auto_walk();
928 self.client.borrow_mut().toggle_sit();
929 }
930 },
931 GameInput::Crawl => {
932 if state && controlling_char {
933 self.stop_auto_walk();
934 self.client.borrow_mut().toggle_crawl();
935 }
936 },
937 GameInput::Dance => {
938 if state && controlling_char {
939 self.stop_auto_walk();
940 self.client.borrow_mut().toggle_dance();
941 }
942 },
943 GameInput::Greet => {
944 if state {
945 self.client.borrow_mut().utter(UtteranceKind::Greeting);
946 }
947 },
948 GameInput::Sneak => {
949 let is_trading = self.client.borrow().is_trading();
950 if state && !is_trading && controlling_char {
951 self.stop_auto_walk();
952 self.client.borrow_mut().toggle_sneak();
953 }
954 },
955 GameInput::CancelClimb => {
956 if state && controlling_char {
957 self.client.borrow_mut().cancel_climb();
958 }
959 },
960 GameInput::MoveForward => {
961 if state && global_state.settings.gameplay.stop_auto_walk_on_input {
962 self.stop_auto_walk();
963 }
964 self.key_state.up = state
965 },
966 GameInput::MoveBack => {
967 if state && global_state.settings.gameplay.stop_auto_walk_on_input {
968 self.stop_auto_walk();
969 }
970 self.key_state.down = state
971 },
972 GameInput::MoveLeft => {
973 if state && global_state.settings.gameplay.stop_auto_walk_on_input {
974 self.stop_auto_walk();
975 }
976 self.key_state.left = state
977 },
978 GameInput::MoveRight => {
979 if state && global_state.settings.gameplay.stop_auto_walk_on_input {
980 self.stop_auto_walk();
981 }
982 self.key_state.right = state
983 },
984 GameInput::Glide => {
985 self.walking_speed = false;
986 let is_trading = self.client.borrow().is_trading();
987 if state && !is_trading && controlling_char {
988 if global_state.settings.gameplay.stop_auto_walk_on_input {
989 self.stop_auto_walk();
990 }
991 self.client.borrow_mut().toggle_glide();
992 }
993 },
994 GameInput::Fly => {
995 self.key_state.fly ^= state;
1001 self.client.borrow_mut().handle_input(
1002 InputKind::Fly,
1003 self.key_state.fly,
1004 None,
1005 self.target_entity,
1006 );
1007 },
1008 GameInput::ToggleWield => {
1009 if state && controlling_char {
1010 let mut client = self.client.borrow_mut();
1011 if client.is_wielding().is_some_and(|b| !b) {
1012 self.walking_speed = false;
1013 }
1014 client.toggle_wield();
1015 }
1016 },
1017 GameInput::SwapLoadout => {
1018 if state && controlling_char {
1019 self.client.borrow_mut().swap_loadout();
1020 }
1021 },
1022 GameInput::ToggleLantern if state && controlling_char => {
1023 let mut client = self.client.borrow_mut();
1024 if client.is_lantern_enabled() {
1025 client.disable_lantern();
1026 } else {
1027 client.enable_lantern();
1028 }
1029 },
1030 GameInput::Mount if state && controlling_char => {
1031 let mut client = self.client.borrow_mut();
1032 if client.is_riding() {
1033 client.unmount();
1034 } else if let Some((_, interactable)) =
1035 self.interactables.input_map.get(&GameInput::Mount)
1036 {
1037 match interactable {
1038 Interactable::Block { volume_pos, .. } => {
1039 client.mount_volume(*volume_pos)
1040 },
1041 Interactable::Entity { entity, .. } => {
1042 client.mount(*entity)
1043 },
1044 }
1045 }
1046 },
1047 GameInput::StayFollow if state => {
1048 let mut client = self.client.borrow_mut();
1049 let player_pos = client
1050 .state()
1051 .read_storage::<Pos>()
1052 .get(client.entity())
1053 .copied();
1054
1055 let mut close_pet = None;
1056 if let Some(player_pos) = player_pos {
1057 let positions = client.state().read_storage::<Pos>();
1058 close_pet = client.state().ecs().read_resource::<CachedSpatialGrid>().0
1059 .in_circle_aabr(player_pos.0.xy(), MAX_MOUNT_RANGE)
1060 .filter(|e|
1061 *e != client.entity()
1062 )
1063 .filter(|e|
1064 matches!(client.state().ecs().read_storage::<comp::Alignment>().get(*e),
1065 Some(comp::Alignment::Owned(owner)) if Some(*owner) == client.uid())
1066 )
1067 .filter(|e|
1068 client.state().ecs().read_storage::<Is<Mount>>().get(*e).is_none()
1069 )
1070 .min_by_key(|e| {
1071 OrderedFloat(positions
1072 .get(*e)
1073 .map_or(MAX_MOUNT_RANGE * MAX_MOUNT_RANGE, |x| {
1074 player_pos.0.distance_squared(x.0)
1075 }
1076 ))
1077 });
1078 }
1079 if let Some(pet_entity) = close_pet
1080 && client
1081 .state()
1082 .read_storage::<Is<Mount>>()
1083 .get(pet_entity)
1084 .is_none()
1085 {
1086 let is_staying = client
1087 .state()
1088 .read_storage::<CharacterActivity>()
1089 .get(pet_entity)
1090 .is_some_and(|activity| activity.is_pet_staying);
1091 client.set_pet_stay(pet_entity, !is_staying);
1092 }
1093 },
1094 GameInput::Interact => {
1095 if state {
1096 let mut client = self.client.borrow_mut();
1097 if let Some((_, interactable)) =
1098 self.interactables.input_map.get(&GameInput::Interact)
1099 {
1100 match interactable {
1101 Interactable::Block {
1102 volume_pos,
1103 block,
1104 interaction,
1105 ..
1106 } => {
1107 match interaction {
1108 BlockInteraction::Collect { .. }
1109 | BlockInteraction::Unlock { .. } => {
1110 if block.is_directly_collectible() {
1111 match volume_pos.kind {
1112 common::mounting::Volume::Terrain => {
1113 client.collect_block(volume_pos.pos);
1114 }
1115 common::mounting::Volume::Entity(_) => {
1116 },
1118 }
1119 }
1120 },
1121 BlockInteraction::Craft(tab) => {
1122 self.hud.show.open_crafting_tab(
1123 *tab,
1124 block
1125 .get_sprite()
1126 .map(|s| (*volume_pos, s)),
1127 )
1128 },
1129 BlockInteraction::Mine(_)
1130 | BlockInteraction::Mount => {},
1131 BlockInteraction::Read(content) => {
1132 match volume_pos.kind {
1133 common::mounting::Volume::Terrain => {
1134 self.hud.show_content_bubble(
1135 volume_pos.pos.as_()
1136 + Vec3::new(
1137 0.5,
1138 0.5,
1139 block.solid_height()
1140 * 0.75,
1141 ),
1142 content.clone(),
1143 )
1144 },
1145 common::mounting::Volume::Entity(_) => {
1148 },
1149 }
1150 },
1151 BlockInteraction::LightToggle(enable) => {
1152 client.toggle_sprite_light(
1153 *volume_pos,
1154 *enable,
1155 );
1156 },
1157 }
1158 },
1159 Interactable::Entity {
1160 entity,
1161 interaction,
1162 ..
1163 } => {
1164 match interaction {
1166 EntityInteraction::HelpDowned => {
1167 client.help_downed(*entity)
1168 },
1169 EntityInteraction::PickupItem => {
1170 client.pick_up(*entity)
1171 },
1172 EntityInteraction::ActivatePortal => {
1173 client.activate_portal(*entity)
1174 },
1175 EntityInteraction::Pet => {
1176 client.do_pet(*entity)
1177 },
1178 EntityInteraction::Talk => {
1179 client.npc_interact(*entity)
1180 },
1181 EntityInteraction::CampfireSit
1182 | EntityInteraction::Trade
1183 | EntityInteraction::StayFollow
1184 | EntityInteraction::Mount => {},
1185 }
1186 },
1187 }
1188 }
1189 }
1190 },
1191 GameInput::Trade => {
1192 if state
1193 && controlling_char
1194 && let Some((_, Interactable::Entity { entity, .. })) =
1195 self.interactables.input_map.get(&GameInput::Trade)
1196 {
1197 let mut client = self.client.borrow_mut();
1198 if let Some(uid) = client.state().ecs().uid_from_entity(*entity)
1199 {
1200 let name = client
1201 .player_list()
1202 .get(&uid)
1203 .map(|info| info.player_alias.clone())
1204 .unwrap_or_else(|| {
1205 let stats = client.state().read_storage::<Stats>();
1206 stats.get(*entity).map_or(
1207 format!("<entity {:?}>", uid),
1208 |e| {
1209 global_state
1210 .i18n
1211 .read()
1212 .get_content(&e.name)
1213 },
1214 )
1215 });
1216
1217 self.hud.new_message(ChatType::Meta.into_msg(
1218 Content::localized_with_args(
1219 "hud-trade-invite_sent",
1220 [("playername", name)],
1221 ),
1222 ));
1223
1224 client.send_invite(uid, InviteKind::Trade)
1225 };
1226 }
1227 },
1228 GameInput::FreeLook => {
1229 let hud = &mut self.hud;
1230 global_state.settings.gameplay.free_look_behavior.update(
1231 state,
1232 &mut self.free_look,
1233 |b| hud.free_look(b),
1234 );
1235 },
1236 GameInput::AutoWalk => {
1237 let hud = &mut self.hud;
1238 global_state.settings.gameplay.auto_walk_behavior.update(
1239 state,
1240 &mut self.auto_walk,
1241 |b| hud.auto_walk(b),
1242 );
1243
1244 self.key_state.auto_walk =
1245 self.auto_walk && !self.client.borrow().is_gliding();
1246 },
1247 GameInput::ZoomIn => {
1248 if state {
1249 if self.zoom_lock {
1250 self.hud.zoom_lock_reminder();
1251 } else {
1252 self.scene.handle_input_event(
1253 Event::Zoom(-30.0),
1254 &self.client.borrow(),
1255 );
1256 }
1257 }
1258 },
1259 GameInput::ZoomOut => {
1260 if state {
1261 if self.zoom_lock {
1262 self.hud.zoom_lock_reminder();
1263 } else {
1264 self.scene.handle_input_event(
1265 Event::Zoom(30.0),
1266 &self.client.borrow(),
1267 );
1268 }
1269 }
1270 },
1271 GameInput::ZoomLock => {
1272 if state {
1273 global_state.settings.gameplay.zoom_lock ^= true;
1274
1275 self.hud
1276 .zoom_lock_toggle(global_state.settings.gameplay.zoom_lock);
1277 }
1278 },
1279 GameInput::CameraClamp => {
1280 let hud = &mut self.hud;
1281 global_state.settings.gameplay.camera_clamp_behavior.update(
1282 state,
1283 &mut self.camera_clamp,
1284 |b| hud.camera_clamp(b),
1285 );
1286 },
1287 GameInput::CycleCamera if state => {
1288 let camera = self.scene.camera_mut();
1295 let client = self.client.borrow();
1296 camera.next_mode(
1297 client.is_moderator(),
1298 (client.presence() != Some(PresenceKind::Spectator))
1299 || self.viewpoint_entity.is_some(),
1300 );
1301 },
1302 GameInput::Select => {
1303 if !state {
1304 self.selected_entity =
1305 self.target_entity.map(|e| (e, std::time::Instant::now()));
1306 }
1307 },
1308 GameInput::AcceptGroupInvite if state => {
1309 let mut client = self.client.borrow_mut();
1310 if client.invite().is_some() {
1311 client.accept_invite();
1312 }
1313 },
1314 GameInput::DeclineGroupInvite if state => {
1315 let mut client = self.client.borrow_mut();
1316 if client.invite().is_some() {
1317 client.decline_invite();
1318 }
1319 },
1320 GameInput::SpectateViewpoint if state => {
1321 let mut client = self.client.borrow_mut();
1322 if self.viewpoint_entity.is_some() {
1323 client.stop_spectate_entity();
1324 self.viewpoint_entity = None;
1325 self.scene.camera_mut().set_mode(CameraMode::Freefly);
1326 let mut ori = self.scene.camera().get_orientation();
1327 ori.z = 0.0;
1330 self.scene.camera_mut().set_orientation(ori);
1331 } else if let Some(target_entity) = entity_target
1332 && self.scene.camera().get_mode() == CameraMode::Freefly
1333 {
1334 client.start_spectate_entity(target_entity.kind.0);
1337
1338 self.viewpoint_entity = Some(target_entity.kind.0);
1339 self.scene.camera_mut().set_mode(CameraMode::FirstPerson);
1340 }
1341 },
1342 GameInput::ToggleWalk if state => {
1343 global_state
1344 .settings
1345 .gameplay
1346 .walking_speed_behavior
1347 .update(state, &mut self.walking_speed, |_| {});
1348 },
1349 _ => {},
1350 }
1351 },
1352 Event::AnalogGameInput(input) => match input {
1353 AnalogGameInput::MovementX(v) => {
1354 self.key_state.analog_matrix.x = v;
1355 },
1356 AnalogGameInput::MovementY(v) => {
1357 self.key_state.analog_matrix.y = v;
1358 },
1359 other => {
1360 self.scene.handle_input_event(
1361 Event::AnalogGameInput(other),
1362 &self.client.borrow(),
1363 );
1364 },
1365 },
1366
1367 Event::ScreenshotMessage(screenshot_msg) => self
1369 .hud
1370 .new_message(ChatType::CommandInfo.into_plain_msg(screenshot_msg)),
1371
1372 Event::Zoom(delta) if self.zoom_lock => {
1373 if delta.abs() > ZOOM_LOCK_SCROLL_DELTA_INTENT {
1375 self.hud.zoom_lock_reminder();
1376 }
1377 },
1378
1379 event => {
1381 self.scene.handle_input_event(event, &self.client.borrow());
1382 }, }
1384 }
1385
1386 if let Some(tgt) = self.hud.current_dialogue() {
1388 let mut client = self.client.borrow_mut();
1389 client.do_talk(Some(tgt));
1390 if matches!(
1392 self.scene.camera().get_mode(),
1393 camera::CameraMode::FirstPerson
1394 ) && let Some(activity) = client
1395 .state()
1396 .read_storage::<CharacterActivity>()
1397 .get(client.entity())
1398 && let Some(dir) = activity.look_dir
1399 {
1400 let ori = Vec3::new(dir.x.atan2(dir.y), -dir.z.atan(), 0.0);
1401 self.scene.camera_mut().lerp_toward(ori, dt, 2.5);
1402 }
1403 }
1404
1405 if let Some(viewpoint_entity) = self.viewpoint_entity
1406 && !self
1407 .client
1408 .borrow()
1409 .state()
1410 .ecs()
1411 .read_storage::<Pos>()
1412 .contains(viewpoint_entity)
1413 {
1414 self.client.borrow_mut().stop_spectate_entity();
1415 self.viewpoint_entity = None;
1416 self.scene.camera_mut().set_mode(CameraMode::Freefly);
1417 }
1418
1419 let (viewpoint_entity, mutable_viewpoint) = self.viewpoint_entity();
1420
1421 let input_vec = self.key_state.dir_vec();
1423 let (axis_right, axis_up) = (input_vec[0], input_vec[1]);
1424
1425 if let Some(ref mut timer) = self.key_state.give_up {
1426 *timer += dt;
1427
1428 if *timer > crate::key_state::GIVE_UP_HOLD_TIME {
1429 self.client.borrow_mut().give_up();
1430 }
1431 }
1432
1433 if mutable_viewpoint {
1434 if let Some(dir) = self
1436 .auto_walk
1437 .then_some(self.client.borrow())
1438 .filter(|client| client.is_gliding())
1439 .and_then(|client| {
1440 let ecs = client.state().ecs();
1441 let entity = client.entity();
1442 let fluid = ecs
1443 .read_storage::<comp::PhysicsState>()
1444 .get(entity)?
1445 .in_fluid?;
1446 let vel = *ecs.read_storage::<Vel>().get(entity)?;
1447 let free_look = self.free_look;
1448 let dir_forward_xy = self.scene.camera().forward_xy();
1449 let dir_right = self.scene.camera().right();
1450
1451 auto_glide(fluid, vel, free_look, dir_forward_xy, dir_right)
1452 })
1453 {
1454 self.key_state.auto_walk = false;
1455 self.inputs.move_dir = Vec2::zero();
1456 self.inputs.look_dir = dir;
1457 } else {
1458 self.key_state.auto_walk = self.auto_walk;
1459 if !self.free_look {
1460 self.walk_forward_dir = self.scene.camera().forward_xy();
1461 self.walk_right_dir = self.scene.camera().right_xy();
1462
1463 let client = self.client.borrow();
1464
1465 let holding_ranged = client
1466 .inventories()
1467 .get(player_entity)
1468 .and_then(|inv| inv.equipped(EquipSlot::ActiveMainhand))
1469 .and_then(|item| item.tool_info())
1470 .is_some_and(|tool_kind| {
1471 matches!(
1472 tool_kind,
1473 ToolKind::Bow
1474 | ToolKind::Staff
1475 | ToolKind::Sceptre
1476 | ToolKind::Throwable
1477 )
1478 })
1479 || client
1480 .current::<CharacterState>()
1481 .is_some_and(|char_state| {
1482 matches!(char_state, CharacterState::Throw(_))
1483 });
1484
1485 let dir = if is_aiming
1486 && holding_ranged
1487 && self.scene.camera().get_mode() == CameraMode::ThirdPerson
1488 {
1489 let ray_start = self.scene.camera().get_focus_pos();
1494 let entity_ray_end = ray_start + cam_dir * 1000.0;
1495 let terrain_ray_end = ray_start + cam_dir * 1000.0;
1496
1497 let aim_point = {
1498 let entity_dist =
1500 ray_entities(&client, ray_start, entity_ray_end, 1000.0).0;
1501 let terrain_ray_distance = client
1502 .state()
1503 .terrain()
1504 .ray(ray_start, terrain_ray_end)
1505 .max_iter(1000)
1506 .until(Block::is_solid)
1507 .cast()
1508 .0;
1509
1510 ray_start + cam_dir * entity_dist.min(terrain_ray_distance)
1512 };
1513
1514 let ori = client
1516 .state()
1517 .read_storage::<comp::Ori>()
1518 .get(player_entity)
1519 .copied()
1520 .unwrap();
1521 let scale = client
1523 .state()
1524 .read_storage::<comp::Scale>()
1525 .get(player_entity)
1526 .copied()
1527 .unwrap_or(comp::Scale(1.0));
1528 let body = client
1530 .state()
1531 .read_storage::<comp::Body>()
1532 .get(player_entity)
1533 .copied()
1534 .unwrap();
1535 let body_offsets = body.projectile_offsets(ori.look_vec(), scale.0);
1536
1537 let player_pos = client
1539 .state()
1540 .read_storage::<Pos>()
1541 .get(player_entity)
1542 .copied()
1543 .unwrap();
1544
1545 drop(client);
1546 aim_point - (player_pos.0 + body_offsets)
1547 } else {
1548 cam_dir + aim_dir_offset
1549 };
1550
1551 self.inputs.look_dir = Dir::from_unnormalized(dir).unwrap();
1552 }
1553 }
1554 self.inputs.strafing = matches!(
1555 self.scene.camera().get_mode(),
1556 camera::CameraMode::FirstPerson
1557 );
1558
1559 if global_state.settings.gameplay.auto_camera
1561 && matches!(
1562 self.scene.camera().get_mode(),
1563 camera::CameraMode::ThirdPerson | camera::CameraMode::FirstPerson
1564 )
1565 && input_vec.magnitude_squared() > 0.0
1566 {
1567 let camera = self.scene.camera_mut();
1568 let ori = camera.get_orientation();
1569 camera.set_orientation_instant(Vec3::new(
1570 ori.x
1571 + input_vec.x
1572 * (3.0 - input_vec.y * 1.5 * if is_aiming { 1.5 } else { 1.0 })
1573 * dt,
1574 std::f32::consts::PI * if is_aiming { 0.015 } else { 0.1 },
1575 0.0,
1576 ));
1577 }
1578
1579 self.inputs.move_z =
1580 self.key_state.swim_up as i32 as f32 - self.key_state.swim_down as i32 as f32;
1581 }
1582
1583 match self.scene.camera().get_mode() {
1584 CameraMode::FirstPerson | CameraMode::ThirdPerson => {
1585 if mutable_viewpoint {
1586 self.inputs.move_dir =
1590 self.walk_right_dir * axis_right + self.walk_forward_dir * axis_up;
1591 }
1592 },
1593 CameraMode::Freefly => {
1594 const FREEFLY_SPEED: f32 = 50.0;
1597 const FREEFLY_SPEED_BOOST: f32 = 5.0;
1598
1599 let forward = self.scene.camera().forward().with_z(0.0).normalized();
1600 let right = self.scene.camera().right().with_z(0.0).normalized();
1601 let up = Vec3::unit_z();
1602 let up_axis = self.key_state.swim_up as i32 as f32
1603 - self.key_state.swim_down as i32 as f32;
1604
1605 let dir = (right * axis_right + forward * axis_up + up * up_axis).normalized();
1606
1607 let speed = FREEFLY_SPEED
1608 * if self.inputs_state.contains(&GameInput::SpectateSpeedBoost) {
1609 FREEFLY_SPEED_BOOST
1610 } else {
1611 1.0
1612 };
1613
1614 let pos = self.scene.camera().get_focus_pos();
1615 self.scene
1616 .camera_mut()
1617 .set_focus_pos(pos + dir * dt * speed);
1618
1619 self.inputs.move_dir = Vec2::zero();
1621 },
1622 };
1623
1624 let mut outcomes = Vec::new();
1625
1626 if !global_state.paused() {
1628 match self.tick(
1630 global_state.clock.get_stable_dt(),
1631 global_state,
1632 &mut outcomes,
1633 ) {
1634 Ok(TickAction::Continue) => {}, Ok(TickAction::Disconnect) => return PlayStateResult::Pop, Err(Error::ClientError(error)) => {
1637 error!("[session] Failed to tick the scene: {:?}", error);
1638 global_state.info_message =
1639 Some(get_client_msg_error(error, None, &global_state.i18n.read()));
1640
1641 return PlayStateResult::Pop;
1642 },
1643 Err(err) => {
1644 global_state.info_message = Some(
1645 global_state
1646 .i18n
1647 .read()
1648 .get_msg("common-connection_lost")
1649 .into_owned(),
1650 );
1651 error!("[session] Failed to tick the scene: {:?}", err);
1652
1653 return PlayStateResult::Pop;
1654 },
1655 }
1656 }
1657
1658 if self.walking_speed {
1659 self.key_state.speed_mul = global_state.settings.gameplay.walking_speed;
1660 } else {
1661 self.key_state.speed_mul = 1.0;
1662 }
1663
1664 self.scene
1666 .camera_mut()
1667 .compute_dependents(&self.client.borrow().state().terrain());
1668
1669 let debug_info = global_state.settings.interface.toggle_debug.then(|| {
1673 let client = self.client.borrow();
1674 let ecs = client.state().ecs();
1675 let client_entity = client.entity();
1676 let coordinates = ecs.read_storage::<Pos>().get(viewpoint_entity).cloned();
1677 let velocity = ecs.read_storage::<Vel>().get(viewpoint_entity).cloned();
1678 let ori = ecs
1679 .read_storage::<comp::Ori>()
1680 .get(viewpoint_entity)
1681 .cloned();
1682 let look_dir = if viewpoint_entity == client_entity {
1685 self.inputs.look_dir
1686 } else {
1687 ecs.read_storage::<comp::Controller>()
1688 .get(viewpoint_entity)
1689 .map(|c| c.inputs.look_dir)
1690 .unwrap_or_default()
1691 };
1692 let in_fluid = ecs
1693 .read_storage::<comp::PhysicsState>()
1694 .get(viewpoint_entity)
1695 .and_then(|state| state.in_fluid);
1696 let character_state = ecs
1697 .read_storage::<comp::CharacterState>()
1698 .get(viewpoint_entity)
1699 .cloned();
1700
1701 DebugInfo {
1702 tps: global_state.clock.stats().average_tps,
1703 frame_time: global_state.clock.stats().average_busy_dt,
1704 ping_ms: self.client.borrow().get_ping_ms_rolling_avg(),
1705 coordinates,
1706 velocity,
1707 ori,
1708 look_dir,
1709 character_state,
1710 in_fluid,
1711 num_chunks: self.scene.terrain().chunk_count() as u32,
1712 num_lights: self.scene.lights().len() as u32,
1713 num_visible_chunks: self.scene.terrain().visible_chunk_count() as u32,
1714 num_shadow_chunks: self.scene.terrain().shadow_chunk_count() as u32,
1715 num_figures: self.scene.figure_mgr().figure_count() as u32,
1716 num_figures_visible: self.scene.figure_mgr().figure_count_visible() as u32,
1717 num_particles: self.scene.particle_mgr().particle_count() as u32,
1718 num_particles_visible: self.scene.particle_mgr().particle_count_visible()
1719 as u32,
1720 current_track: self.scene.music_mgr().current_track(),
1721 current_artist: self.scene.music_mgr().current_artist(),
1722 active_channels: global_state.audio.get_num_active_channels(),
1723 audio_cpu_usage: global_state.audio.get_cpu_usage(),
1724 }
1725 });
1726
1727 let inverted_interactable_map = self.interactables.inverted_map();
1728
1729 let mut hud_events = self.hud.maintain(
1731 &self.client.borrow(),
1732 global_state,
1733 &debug_info,
1734 self.scene.camera(),
1735 global_state.clock.get_stable_dt(),
1736 HudInfo {
1737 is_aiming,
1738 active_mine_tool,
1739 is_first_person: matches!(
1740 self.scene.camera().get_mode(),
1741 camera::CameraMode::FirstPerson
1742 ),
1743 viewpoint_entity,
1744 mutable_viewpoint,
1745 target_entity: self.target_entity,
1746 selected_entity: self.selected_entity,
1747 persistence_load_error: self.metadata.skill_set_persistence_load_error,
1748 key_state: &self.key_state,
1749 },
1750 inverted_interactable_map,
1751 );
1752
1753 #[cfg(feature = "egui-ui")]
1755 if global_state.settings.interface.egui_enabled() {
1756 let settings_change = global_state.egui_state.maintain(
1757 &mut self.client.borrow_mut(),
1758 &mut self.scene,
1759 debug_info.map(|debug_info| EguiDebugInfo {
1760 frame_time: debug_info.frame_time,
1761 ping_ms: debug_info.ping_ms,
1762 }),
1763 &global_state.settings,
1764 );
1765
1766 if let Some(settings_change) = settings_change {
1767 settings_change.process(global_state, self);
1768 }
1769 }
1770
1771 if global_state.i18n.reloaded() {
1773 hud_events.push(HudEvent::SettingsChange(
1774 ChangeLanguage(Box::new(global_state.i18n.read().metadata().clone())).into(),
1775 ));
1776 }
1777
1778 let mut has_repaired = false;
1779 let sfx_triggers = self.scene.sfx_mgr.triggers.read();
1780 for event in hud_events {
1782 match event {
1783 HudEvent::SendMessage(msg) => {
1784 self.client.borrow_mut().send_chat(msg);
1786 },
1787 HudEvent::SendCommand(name, args) => {
1788 match run_command(self, global_state, &name, args) {
1789 Ok(Some(info)) => {
1790 self.hud.new_message(ChatType::CommandInfo.into_msg(info))
1791 },
1792 Ok(None) => {}, Err(error) => {
1794 self.hud.new_message(ChatType::CommandError.into_msg(error))
1795 },
1796 };
1797 },
1798 HudEvent::CharacterSelection => {
1799 global_state.audio.stop_all_music();
1800 global_state.audio.stop_all_ambience();
1801 global_state.audio.stop_all_sfx();
1802 self.client.borrow_mut().request_remove_character()
1803 },
1804 HudEvent::Logout => {
1805 self.client.borrow_mut().logout();
1806 global_state.audio.stop_all_ambience();
1810 global_state.audio.stop_all_sfx();
1811 return PlayStateResult::Pop;
1812 },
1813 HudEvent::Quit => {
1814 return PlayStateResult::Shutdown;
1815 },
1816
1817 HudEvent::RemoveBuff(buff_id) => {
1818 self.client.borrow_mut().remove_buff(buff_id);
1819 },
1820 HudEvent::LeaveStance => self.client.borrow_mut().leave_stance(),
1821 HudEvent::UnlockSkill(skill) => {
1822 self.client.borrow_mut().unlock_skill(skill);
1823 },
1824 HudEvent::UseSlot {
1825 slot,
1826 bypass_dialog,
1827 } => {
1828 let mut move_allowed = true;
1829
1830 if !bypass_dialog
1831 && let Some(inventory) = self
1832 .client
1833 .borrow()
1834 .state()
1835 .ecs()
1836 .read_storage::<comp::Inventory>()
1837 .get(self.client.borrow().entity())
1838 {
1839 match slot {
1840 Slot::Inventory(inv_slot) => {
1841 let slot_deficit = inventory.free_after_equip(inv_slot);
1842 if slot_deficit < 0 {
1843 self.hud.set_prompt_dialog(PromptDialogSettings::new(
1844 global_state.i18n.read().get_content(
1845 &Content::localized_with_args(
1846 "hud-bag-use_slot_equip_drop_items",
1847 [(
1848 "slot_deficit",
1849 slot_deficit.unsigned_abs() as u64,
1850 )],
1851 ),
1852 ),
1853 HudEvent::UseSlot {
1854 slot,
1855 bypass_dialog: true,
1856 },
1857 None,
1858 ));
1859 move_allowed = false;
1860 }
1861 },
1862 Slot::Equip(equip_slot) => {
1863 let free_slots =
1866 inventory.free_slots_minus_equipped_item(equip_slot);
1867 if free_slots > 0 {
1868 let slot_deficit = inventory.free_after_unequip(equip_slot);
1869 if slot_deficit < 0 {
1870 self.hud.set_prompt_dialog(PromptDialogSettings::new(
1871 global_state.i18n.read().get_content(
1872 &Content::localized_with_args(
1873 "hud-bag-use_slot_unequip_drop_items",
1874 [(
1875 "slot_deficit",
1876 slot_deficit.unsigned_abs() as u64,
1877 )],
1878 ),
1879 ),
1880 HudEvent::UseSlot {
1881 slot,
1882 bypass_dialog: true,
1883 },
1884 None,
1885 ));
1886 move_allowed = false;
1887 }
1888 } else {
1889 move_allowed = false;
1890 }
1891 },
1892 Slot::Overflow(_) => {},
1893 }
1894 };
1895
1896 if move_allowed {
1897 self.client.borrow_mut().use_slot(slot);
1898 }
1899 },
1900 HudEvent::SwapEquippedWeapons => {
1901 self.client.borrow_mut().swap_loadout();
1902 },
1903 HudEvent::SwapSlots {
1904 slot_a,
1905 slot_b,
1906 bypass_dialog,
1907 } => {
1908 let mut move_allowed = true;
1909 if !bypass_dialog
1910 && let Some(inventory) = self
1911 .client
1912 .borrow()
1913 .state()
1914 .ecs()
1915 .read_storage::<comp::Inventory>()
1916 .get(self.client.borrow().entity())
1917 {
1918 match (slot_a, slot_b) {
1919 (Slot::Inventory(inv_slot), Slot::Equip(equip_slot))
1920 | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => {
1921 if !inventory.can_swap(inv_slot, equip_slot) {
1922 move_allowed = false;
1923 } else {
1924 let slot_deficit =
1925 inventory.free_after_swap(equip_slot, inv_slot);
1926 if slot_deficit < 0 {
1927 self.hud.set_prompt_dialog(PromptDialogSettings::new(
1928 global_state.i18n.read().get_content(
1929 &Content::localized_with_args(
1930 "hud-bag-swap_slots_drop_items",
1931 [(
1932 "slot_deficit",
1933 slot_deficit.unsigned_abs() as u64,
1934 )],
1935 ),
1936 ),
1937 HudEvent::SwapSlots {
1938 slot_a,
1939 slot_b,
1940 bypass_dialog: true,
1941 },
1942 None,
1943 ));
1944 move_allowed = false;
1945 }
1946 }
1947 },
1948 _ => {},
1949 }
1950 }
1951 if move_allowed {
1952 self.client.borrow_mut().swap_slots(slot_a, slot_b);
1953 }
1954 },
1955 HudEvent::SelectExpBar(skillgroup) => {
1956 global_state.settings.interface.xp_bar_skillgroup = skillgroup;
1957 },
1958 HudEvent::SplitSwapSlots {
1959 slot_a,
1960 slot_b,
1961 bypass_dialog,
1962 } => {
1963 let mut move_allowed = true;
1964 if !bypass_dialog
1965 && let Some(inventory) = self
1966 .client
1967 .borrow()
1968 .state()
1969 .ecs()
1970 .read_storage::<comp::Inventory>()
1971 .get(self.client.borrow().entity())
1972 {
1973 match (slot_a, slot_b) {
1974 (Slot::Inventory(inv_slot), Slot::Equip(equip_slot))
1975 | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => {
1976 if !inventory.can_swap(inv_slot, equip_slot) {
1977 move_allowed = false;
1978 } else {
1979 let slot_deficit =
1980 inventory.free_after_swap(equip_slot, inv_slot);
1981 if slot_deficit < 0 {
1982 self.hud.set_prompt_dialog(PromptDialogSettings::new(
1983 global_state.i18n.read().get_content(
1984 &Content::localized_with_args(
1985 "hud-bag-split_swap_slots_drop_items",
1986 [(
1987 "slot_deficit",
1988 slot_deficit.unsigned_abs() as u64,
1989 )],
1990 ),
1991 ),
1992 HudEvent::SwapSlots {
1993 slot_a,
1994 slot_b,
1995 bypass_dialog: true,
1996 },
1997 None,
1998 ));
1999 move_allowed = false;
2000 }
2001 }
2002 },
2003 _ => {},
2004 }
2005 };
2006 if move_allowed {
2007 self.client.borrow_mut().split_swap_slots(slot_a, slot_b);
2008 }
2009 },
2010 HudEvent::DropSlot(x) => {
2011 let mut client = self.client.borrow_mut();
2012 client.drop_slot(x);
2013 if let Slot::Equip(EquipSlot::Lantern) = x {
2014 client.disable_lantern();
2015 }
2016 },
2017 HudEvent::SplitDropSlot(x) => {
2018 let mut client = self.client.borrow_mut();
2019 client.split_drop_slot(x);
2020 if let Slot::Equip(EquipSlot::Lantern) = x {
2021 client.disable_lantern();
2022 }
2023 },
2024 HudEvent::SortInventory(sort_order) => {
2025 self.client.borrow_mut().sort_inventory(sort_order);
2026 },
2027 HudEvent::ChangeHotbarState(state) => {
2028 let client = self.client.borrow();
2029
2030 let server_name = &client.server_info().name;
2031 let character_id = match client.presence().unwrap() {
2033 PresenceKind::Character(id) => Some(id),
2034 PresenceKind::LoadingCharacter(id) => Some(id),
2035 PresenceKind::Spectator => {
2036 unreachable!("HUD adaption in Spectator mode!")
2037 },
2038 PresenceKind::Possessor => None,
2039 };
2040
2041 global_state.profile.set_hotbar_slots(
2043 server_name,
2044 character_id,
2045 state.slots,
2046 );
2047
2048 global_state
2049 .profile
2050 .save_to_file_warn(&global_state.config_dir);
2051
2052 info!("Event! -> ChangedHotbarState")
2053 },
2054 HudEvent::TradeAction(action) => {
2055 self.client.borrow_mut().perform_trade_action(action);
2056 },
2057 HudEvent::Ability { idx, state } => {
2058 self.client.borrow_mut().handle_input(
2059 InputKind::Ability(idx),
2060 state,
2061 default_select_pos,
2062 self.target_entity,
2063 );
2064 },
2065
2066 HudEvent::RequestSiteInfo(id) => {
2067 self.client.borrow_mut().request_site_economy(id);
2068 },
2069
2070 HudEvent::CraftRecipe {
2071 recipe_name: recipe,
2072 craft_sprite,
2073 amount,
2074 } => {
2075 let slots = {
2076 let client = self.client.borrow();
2077
2078 if let Some(inventory) = client
2079 .state()
2080 .ecs()
2081 .read_storage::<comp::Inventory>()
2082 .get(client.entity())
2083 {
2084 let rbm =
2085 client.state().ecs().read_resource::<RecipeBookManifest>();
2086 if let Some(recipe) = inventory.get_recipe(&recipe, &rbm) {
2087 recipe.inventory_contains_ingredients(inventory, 1).ok()
2088 } else {
2089 None
2090 }
2091 } else {
2092 None
2093 }
2094 };
2095 if let Some(slots) = slots {
2096 self.client.borrow_mut().craft_recipe(
2097 &recipe,
2098 slots,
2099 craft_sprite,
2100 amount,
2101 );
2102 }
2103 },
2104
2105 HudEvent::CraftModularWeapon {
2106 primary_slot,
2107 secondary_slot,
2108 craft_sprite,
2109 } => {
2110 self.client.borrow_mut().craft_modular_weapon(
2111 primary_slot,
2112 secondary_slot,
2113 craft_sprite,
2114 );
2115 },
2116
2117 HudEvent::CraftModularWeaponComponent {
2118 toolkind,
2119 material,
2120 modifier,
2121 craft_sprite,
2122 } => {
2123 let additional_slots = {
2124 let client = self.client.borrow();
2125 let item_id = |slot| {
2126 client
2127 .inventories()
2128 .get(client.entity())
2129 .and_then(|inv| inv.get(slot))
2130 .and_then(|item| {
2131 item.item_definition_id().itemdef_id().map(String::from)
2132 })
2133 };
2134 if let Some(material_id) = item_id(material) {
2135 let key = recipe::ComponentKey {
2136 toolkind,
2137 material: material_id,
2138 modifier: modifier.and_then(item_id),
2139 };
2140 if let Some(recipe) = client.component_recipe_book().get(&key) {
2141 client.inventories().get(client.entity()).and_then(|inv| {
2142 recipe.inventory_contains_additional_ingredients(inv).ok()
2143 })
2144 } else {
2145 None
2146 }
2147 } else {
2148 None
2149 }
2150 };
2151 if let Some(additional_slots) = additional_slots {
2152 self.client.borrow_mut().craft_modular_weapon_component(
2153 toolkind,
2154 material,
2155 modifier,
2156 additional_slots,
2157 craft_sprite,
2158 );
2159 }
2160 },
2161 HudEvent::SalvageItem { slot, salvage_pos } => {
2162 self.client.borrow_mut().salvage_item(slot, salvage_pos);
2163 },
2164 HudEvent::RepairItem { item, sprite_pos } => {
2165 let slots = {
2166 let client = self.client.borrow();
2167 let slots = (|| {
2168 if let Some(inventory) = client.inventories().get(client.entity()) {
2169 let item = match item {
2170 Slot::Equip(slot) => inventory.equipped(slot),
2171 Slot::Inventory(slot) => inventory.get(slot),
2172 Slot::Overflow(_) => None,
2173 }?;
2174 let repair_recipe =
2175 client.repair_recipe_book().repair_recipe(item)?;
2176 repair_recipe
2177 .inventory_contains_ingredients(item, inventory)
2178 .ok()
2179 } else {
2180 None
2181 }
2182 })();
2183 slots.unwrap_or_default()
2184 };
2185 if !has_repaired {
2186 let sfx_trigger_item = sfx_triggers
2187 .0
2188 .get_key_value(&SfxEvent::from(&InventoryUpdateEvent::Craft));
2189 global_state.audio.emit_ui_sfx(sfx_trigger_item, None, None);
2190 has_repaired = true
2191 };
2192 self.client
2193 .borrow_mut()
2194 .repair_item(item, slots, sprite_pos);
2195 },
2196 HudEvent::InviteMember(uid) => {
2197 self.client.borrow_mut().send_invite(uid, InviteKind::Group);
2198 },
2199 HudEvent::AcceptInvite => {
2200 self.client.borrow_mut().accept_invite();
2201 },
2202 HudEvent::DeclineInvite => {
2203 self.client.borrow_mut().decline_invite();
2204 },
2205 HudEvent::KickMember(uid) => {
2206 self.client.borrow_mut().kick_from_group(uid);
2207 },
2208 HudEvent::LeaveGroup => {
2209 self.client.borrow_mut().leave_group();
2210 },
2211 HudEvent::AssignLeader(uid) => {
2212 self.client.borrow_mut().assign_group_leader(uid);
2213 },
2214 HudEvent::ChangeAbility(slot, new_ability) => {
2215 self.client.borrow_mut().change_ability(slot, new_ability);
2216 },
2217 HudEvent::SettingsChange(settings_change) => {
2218 settings_change.process(global_state, self);
2219 },
2220 HudEvent::AcknowledgePersistenceLoadError => {
2221 self.metadata.skill_set_persistence_load_error = None;
2222 },
2223 HudEvent::MapMarkerEvent(event) => {
2224 self.client.borrow_mut().map_marker_event(event);
2225 },
2226 HudEvent::Dialogue(target, dialogue) => {
2227 self.client.borrow_mut().perform_dialogue(target, dialogue);
2228 },
2229 HudEvent::SetBattleMode(mode) => {
2230 self.client.borrow_mut().set_battle_mode(mode);
2231 },
2232 }
2233 }
2234
2235 {
2236 let client = self.client.borrow();
2237 let scene_data = SceneData {
2238 client: &client,
2239 state: client.state(),
2240 viewpoint_entity,
2241 mutable_viewpoint: mutable_viewpoint || self.free_look,
2242 target_entities: &self.interactables.entities,
2244 loaded_distance: client.loaded_distance(),
2245 terrain_view_distance: client.view_distance().unwrap_or(1),
2246 entity_view_distance: client
2247 .view_distance()
2248 .unwrap_or(1)
2249 .min(global_state.settings.graphics.entity_view_distance),
2250 tick: client.get_tick(),
2251 gamma: global_state.settings.graphics.gamma,
2252 exposure: global_state.settings.graphics.exposure,
2253 ambiance: global_state.settings.graphics.ambiance,
2254 mouse_smoothing: global_state.settings.gameplay.smooth_pan_enable,
2255 sprite_render_distance: global_state.settings.graphics.sprite_render_distance
2256 as f32,
2257 particles_enabled: global_state.settings.graphics.particles_enabled,
2258 weapon_trails_enabled: global_state.settings.graphics.weapon_trails_enabled,
2259 flashing_lights_enabled: global_state
2260 .settings
2261 .graphics
2262 .render_mode
2263 .flashing_lights_enabled,
2264 figure_lod_render_distance: global_state
2265 .settings
2266 .graphics
2267 .figure_lod_render_distance
2268 as f32,
2269 is_aiming,
2270 interpolated_time_of_day: self.scene.interpolated_time_of_day,
2271 wind_vel: self.scene.wind_vel,
2272 };
2273
2274 if !global_state.paused() {
2276 self.scene.maintain(
2277 global_state.window.renderer_mut(),
2278 &mut global_state.audio,
2279 &scene_data,
2280 &client,
2281 &global_state.settings,
2282 );
2283
2284 for outcome in outcomes {
2286 self.scene
2287 .handle_outcome(&outcome, &scene_data, &mut global_state.audio);
2288 self.hud.handle_outcome(&outcome, &scene_data, global_state);
2289 }
2290 }
2291 }
2292
2293 self.cleanup();
2295
2296 PlayStateResult::Continue
2297 } else if client_registered && client_presence.is_none() {
2298 if client_type.can_spectate() && !client_type.can_enter_character() {
2301 PlayStateResult::Pop
2303 } else {
2304 PlayStateResult::Switch(Box::new(CharSelectionState::new(
2305 global_state,
2306 Rc::clone(&self.client),
2307 Rc::clone(&self.hud.persisted_state),
2308 )))
2309 }
2310 } else {
2311 error!("Client not in the expected state, exiting session play state");
2312 PlayStateResult::Pop
2313 }
2314 }
2315
2316 fn name(&self) -> &'static str { "Session" }
2317
2318 fn capped_fps(&self) -> bool { false }
2319
2320 fn globals_bind_group(&self) -> &GlobalsBindGroup { self.scene.global_bind_group() }
2321
2322 fn render(&self, drawer: &mut Drawer<'_>, settings: &Settings) {
2326 span!(_guard, "render", "<Session as PlayState>::render");
2327
2328 let client = self.client.borrow();
2329
2330 let (viewpoint_entity, mutable_viewpoint) = self.viewpoint_entity();
2331
2332 let scene_data = SceneData {
2333 client: &client,
2334 state: client.state(),
2335 viewpoint_entity,
2336 mutable_viewpoint,
2337 target_entities: &self.interactables.entities,
2339 loaded_distance: client.loaded_distance(),
2340 terrain_view_distance: client.view_distance().unwrap_or(1),
2341 entity_view_distance: client
2342 .view_distance()
2343 .unwrap_or(1)
2344 .min(settings.graphics.entity_view_distance),
2345 tick: client.get_tick(),
2346 gamma: settings.graphics.gamma,
2347 exposure: settings.graphics.exposure,
2348 ambiance: settings.graphics.ambiance,
2349 mouse_smoothing: settings.gameplay.smooth_pan_enable,
2350 sprite_render_distance: settings.graphics.sprite_render_distance as f32,
2351 figure_lod_render_distance: settings.graphics.figure_lod_render_distance as f32,
2352 particles_enabled: settings.graphics.particles_enabled,
2353 weapon_trails_enabled: settings.graphics.weapon_trails_enabled,
2354 flashing_lights_enabled: settings.graphics.render_mode.flashing_lights_enabled,
2355 is_aiming: self.is_aiming,
2356 interpolated_time_of_day: self.scene.interpolated_time_of_day,
2357 wind_vel: self.scene.wind_vel,
2358 };
2359
2360 self.scene.render(
2362 drawer,
2363 client.state(),
2364 viewpoint_entity,
2365 client.get_tick(),
2366 &scene_data,
2367 );
2368
2369 if let Some(mut volumetric_pass) = drawer.volumetric_pass() {
2370 prof_span!("clouds");
2372 volumetric_pass.draw_clouds();
2373 }
2374 if let Some(mut transparent_pass) = drawer.transparent_pass() {
2375 prof_span!("trails");
2377 if let Some(mut trail_drawer) = transparent_pass.draw_trails() {
2378 self.scene
2379 .trail_mgr()
2380 .render(&mut trail_drawer, &scene_data);
2381 }
2382 }
2383 {
2385 prof_span!("bloom");
2386 drawer.run_bloom_passes()
2387 }
2388 {
2390 prof_span!("post-process and ui");
2391 let mut third_pass = drawer.third_pass();
2392 third_pass.draw_postprocess();
2393 if let Some(mut ui_drawer) = third_pass.draw_ui() {
2395 self.hud.render(&mut ui_drawer);
2396 }; }
2399 }
2400
2401 fn egui_enabled(&self) -> bool { true }
2402}
2403
2404fn find_shortest_distance(arr: &[Option<f32>]) -> Option<f32> {
2405 arr.iter()
2406 .filter_map(|x| *x)
2407 .min_by(|d1, d2| OrderedFloat(*d1).cmp(&OrderedFloat(*d2)))
2408}
2409
2410fn auto_glide(
2412 fluid: Fluid,
2413 vel: Vel,
2414 free_look: bool,
2415 dir_forward_xy: Vec2<f32>,
2416 dir_right: Vec3<f32>,
2417) -> Option<Dir> {
2418 let Vel(rel_flow) = fluid.relative_flow(&vel);
2419
2420 let is_wind_downwards = rel_flow.z.is_sign_negative();
2421
2422 let dir = if free_look {
2423 if is_wind_downwards {
2424 Vec3::from(-rel_flow.xy())
2425 } else {
2426 -rel_flow
2427 }
2428 } else if is_wind_downwards {
2429 dir_forward_xy.into()
2430 } else {
2431 let windwards = rel_flow * dir_forward_xy.dot(rel_flow.xy()).signum();
2432 Plane::from(Dir::new(dir_right)).projection(windwards)
2433 };
2434
2435 Dir::from_unnormalized(dir)
2436}