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