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