1#![deny(unsafe_code)]
2#![deny(clippy::clone_on_ref_ptr)]
3
4pub mod addr;
5pub mod error;
6
7pub use crate::error::Error;
9pub use authc::AuthClientError;
10pub use common_net::msg::ServerInfo;
11pub use specs::{
12 Builder, DispatcherBuilder, Entity as EcsEntity, Join, LendJoin, ReadStorage, World, WorldExt,
13};
14
15use crate::addr::ConnectionArgs;
16use byteorder::{ByteOrder, LittleEndian};
17use common::{
18 character::{CharacterId, CharacterItem},
19 comp::{
20 self, AdminRole, CharacterState, ChatMode, ControlAction, ControlEvent, Controller,
21 ControllerInputs, GroupManip, Hardcore, InputKind, InventoryAction, InventoryEvent,
22 InventoryUpdateEvent, MapMarkerChange, PresenceKind, UtteranceKind,
23 chat::KillSource,
24 controller::CraftEvent,
25 gizmos::Gizmos,
26 group,
27 inventory::{
28 InventorySortOrder,
29 item::{ItemKind, modular, tool},
30 },
31 invite::{InviteKind, InviteResponse},
32 skills::Skill,
33 slot::{EquipSlot, InvSlotId, Slot},
34 },
35 event::{EventBus, LocalEvent, PluginHash, UpdateCharacterMetadata},
36 grid::Grid,
37 link::Is,
38 lod,
39 map::Marker,
40 mounting::{Rider, VolumePos, VolumeRider},
41 outcome::Outcome,
42 recipe::{ComponentRecipeBook, RecipeBookManifest},
43 resources::{BattleMode, GameMode, PlayerEntity, Time, TimeOfDay},
44 rtsim,
45 shared_server_config::ServerConstants,
46 spiral::Spiral2d,
47 terrain::{
48 BiomeKind, CoordinateConversions, SiteKindMeta, SpriteKind, TerrainChunk, TerrainChunkSize,
49 TerrainGrid, block::Block, map::MapConfig, neighbors,
50 },
51 trade::{PendingTrade, SitePrices, TradeAction, TradeId, TradeResult},
52 uid::{IdMaps, Uid},
53 vol::RectVolSize,
54 weather::{CompressedWeather, SharedWeatherGrid, Weather, WeatherGrid},
55};
56#[cfg(feature = "tracy")] use common_base::plot;
57use common_base::{prof_span, span};
58use common_i18n::Content;
59use common_net::{
60 msg::{
61 ChatTypeContext, ClientGeneral, ClientMsg, ClientRegister, DisconnectReason, InviteAnswer,
62 Notification, PingMsg, PlayerInfo, PlayerListUpdate, RegisterError, ServerGeneral,
63 ServerInit, ServerRegisterAnswer,
64 server::ServerDescription,
65 world_msg::{EconomyInfo, PoiInfo, SiteId},
66 },
67 sync::WorldSyncExt,
68};
69
70pub use common_net::msg::ClientType;
71use common_state::State;
72#[cfg(feature = "plugins")]
73use common_state::plugin::PluginMgr;
74use common_systems::add_local_systems;
75use comp::BuffKind;
76use hashbrown::{HashMap, HashSet};
77use hickory_resolver::{
78 Resolver, config::ResolverConfig, net::runtime::TokioRuntimeProvider, proto::rr::RData,
79};
80use image::DynamicImage;
81use network::{ConnectAddr, Network, Participant, Pid, Stream};
82use num::traits::FloatConst;
83use rayon::prelude::*;
84use rustls::client::danger::ServerCertVerified;
85use specs::Component;
86use std::{
87 collections::{BTreeMap, VecDeque},
88 fmt::Debug,
89 mem,
90 path::PathBuf,
91 sync::Arc,
92 time::{Duration, Instant},
93};
94use tokio::runtime::Runtime;
95use tracing::{debug, error, trace, warn};
96use vek::*;
97
98pub const MAX_SELECTABLE_VIEW_DISTANCE: u32 = 65;
99
100const PING_ROLLING_AVERAGE_SECS: usize = 10;
101
102#[derive(Debug)]
106pub enum Event {
107 Chat(comp::ChatMsg),
108 GroupInventoryUpdate(comp::FrontendItem, Uid),
109 InviteComplete {
110 target: Uid,
111 answer: InviteAnswer,
112 kind: InviteKind,
113 },
114 TradeComplete {
115 result: TradeResult,
116 trade: PendingTrade,
117 },
118 Disconnect,
119 DisconnectionNotification(u64),
120 InventoryUpdated(Vec<InventoryUpdateEvent>),
121 Notification(UserNotification),
122 SetViewDistance(u32),
123 Outcome(Outcome),
124 CharacterCreated(CharacterId),
125 CharacterEdited(CharacterId),
126 CharacterJoined(UpdateCharacterMetadata),
127 CharacterError(String),
128 MapMarker(comp::MapMarkerUpdate),
129 StartSpectate(Vec3<f32>),
130 SpectatePosition(Vec3<f32>),
131 PluginDataReceived(Vec<u8>),
132 Dialogue(Uid, rtsim::Dialogue<true>),
133 Gizmos(Vec<Gizmos>),
134}
135
136#[derive(Debug)]
141pub enum UserNotification {
142 WaypointUpdated,
143}
144
145#[derive(Debug)]
146pub enum ClientInitStage {
147 ConnectionEstablish,
149 WatingForServerVersion,
151 Authentication,
153 LoadingInitData,
156 StartingClient,
159}
160
161pub struct WorldData {
162 pub lod_base: Grid<u32>,
166 pub lod_alt: Grid<u32>,
170 pub lod_horizon: Grid<u32>,
174 map: (Vec<Arc<DynamicImage>>, Vec2<u16>, Vec2<f32>),
185}
186
187impl WorldData {
188 pub fn chunk_size(&self) -> Vec2<u16> { self.map.1 }
189
190 pub fn map_layers(&self) -> &Vec<Arc<DynamicImage>> { &self.map.0 }
191
192 pub fn map_image(&self) -> &Arc<DynamicImage> { &self.map.0[0] }
193
194 pub fn topo_map_image(&self) -> &Arc<DynamicImage> { &self.map.0[1] }
195
196 pub fn min_chunk_alt(&self) -> f32 { self.map.2.x }
197
198 pub fn max_chunk_alt(&self) -> f32 { self.map.2.y }
199}
200
201pub struct SiteMarker {
202 pub marker: Marker,
203 pub economy: Option<EconomyInfo>,
204}
205
206struct WeatherLerp {
207 old: (SharedWeatherGrid, Instant),
208 new: (SharedWeatherGrid, Instant),
209 old_local_wind: (Vec2<f32>, Instant),
210 new_local_wind: (Vec2<f32>, Instant),
211 local_wind: Vec2<f32>,
212}
213
214impl WeatherLerp {
215 fn local_wind_update(&mut self, wind: Vec2<f32>) {
216 self.old_local_wind = mem::replace(&mut self.new_local_wind, (wind, Instant::now()));
217 }
218
219 fn update_local_wind(&mut self) {
220 let t = (self.new_local_wind.1.elapsed().as_secs_f32()
222 / self
223 .new_local_wind
224 .1
225 .duration_since(self.old_local_wind.1)
226 .as_secs_f32())
227 .clamp(0.0, 1.0);
228
229 self.local_wind = Vec2::lerp_unclamped(self.old_local_wind.0, self.new_local_wind.0, t);
230 }
231
232 fn weather_update(&mut self, weather: SharedWeatherGrid) {
233 self.old = mem::replace(&mut self.new, (weather, Instant::now()));
234 }
235
236 fn update(&mut self, to_update: &mut WeatherGrid) {
239 prof_span!("WeatherLerp::update");
240 self.update_local_wind();
241 let old = &self.old.0;
242 let new = &self.new.0;
243 if new.size() == Vec2::zero() {
244 return;
245 }
246 if to_update.size() != new.size() {
247 *to_update = WeatherGrid::from(new);
248 }
249 if old.size() == new.size() {
250 let t = (self.new.1.elapsed().as_secs_f32()
252 / self.new.1.duration_since(self.old.1).as_secs_f32())
253 .clamp(0.0, 1.0);
254
255 to_update
256 .iter_mut()
257 .zip(old.iter().zip(new.iter()))
258 .for_each(|((_, current), ((_, old), (_, new)))| {
259 *current = CompressedWeather::lerp_unclamped(old, new, t);
260 current.wind = self.local_wind;
263 });
264 }
265 }
266}
267
268impl Default for WeatherLerp {
269 fn default() -> Self {
270 let old = Instant::now();
271 let new = Instant::now();
272 Self {
273 old: (SharedWeatherGrid::new(Vec2::zero()), old),
274 new: (SharedWeatherGrid::new(Vec2::zero()), new),
275 old_local_wind: (Vec2::zero(), old),
276 new_local_wind: (Vec2::zero(), new),
277 local_wind: Vec2::zero(),
278 }
279 }
280}
281
282pub struct Client {
283 client_type: ClientType,
284 registered: bool,
285 presence: Option<PresenceKind>,
286 runtime: Arc<Runtime>,
287 server_info: ServerInfo,
288 server_description: ServerDescription,
290 world_data: WorldData,
291 weather: WeatherLerp,
292 player_list: HashMap<Uid, PlayerInfo>,
293 character_list: CharacterList,
294 character_being_deleted: Option<CharacterId>,
295 sites: HashMap<SiteId, SiteMarker>,
296 extra_markers: Vec<Marker>,
297 possible_starting_sites: Vec<SiteId>,
298 pois: Vec<PoiInfo>,
299 pub chat_mode: ChatMode,
300 component_recipe_book: ComponentRecipeBook,
301 available_recipes: HashMap<String, Option<SpriteKind>>,
302 lod_zones: HashMap<Vec2<i32>, lod::Zone>,
303 lod_last_requested: Option<Instant>,
304 lod_pos_fallback: Option<Vec2<f32>>,
305 force_update_counter: u64,
306
307 role: Option<AdminRole>,
308 max_group_size: u32,
309 invite: Option<(Uid, Instant, Duration, InviteKind)>,
311 group_leader: Option<Uid>,
312 group_members: HashMap<Uid, group::Role>,
314 pending_invites: HashSet<Uid>,
316 pending_trade: Option<(TradeId, PendingTrade, Option<SitePrices>)>,
318 waypoint: Option<String>,
319
320 network: Option<Network>,
321 participant: Option<Participant>,
322 general_stream: Stream,
323 ping_stream: Stream,
324 register_stream: Stream,
325 character_screen_stream: Stream,
326 in_game_stream: Stream,
327 terrain_stream: Stream,
328
329 client_timeout: Duration,
330 last_server_ping: f64,
331 last_server_pong: f64,
332 last_ping_delta: f64,
333 ping_deltas: VecDeque<f64>,
334
335 tick: u64,
336 state: State,
337
338 flashing_lights_enabled: bool,
339
340 server_view_distance_limit: Option<u32>,
342 view_distance: Option<u32>,
343 lod_distance: f32,
344 loaded_distance: f32,
346
347 pending_chunks: HashMap<Vec2<i32>, Instant>,
348 target_time_of_day: Option<TimeOfDay>,
349 dt_adjustment: f64,
350
351 connected_server_constants: ServerConstants,
352 missing_plugins: HashSet<PluginHash>,
354 local_plugins: Vec<PathBuf>,
356}
357
358#[derive(Debug, Default)]
361pub struct CharacterList {
362 pub characters: Vec<CharacterItem>,
363 pub loading: bool,
364}
365
366async fn connect_quic(
367 network: &Network,
368 hostname: String,
369 override_port: Option<u16>,
370 prefer_ipv6: bool,
371 validate_tls: bool,
372) -> Result<network::Participant, crate::error::Error> {
373 let config = if validate_tls {
374 quinn::ClientConfig::try_with_platform_verifier()?
375 } else {
376 warn!(
377 "skipping validation of server identity. There is no guarantee that the server you're \
378 connected to is the one you expect to be connecting to."
379 );
380 #[derive(Debug)]
381 struct Verifier;
382 impl rustls::client::danger::ServerCertVerifier for Verifier {
383 fn verify_server_cert(
384 &self,
385 _end_entity: &rustls::pki_types::CertificateDer<'_>,
386 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
387 _server_name: &rustls::pki_types::ServerName<'_>,
388 _ocsp_response: &[u8],
389 _now: rustls::pki_types::UnixTime,
390 ) -> Result<ServerCertVerified, rustls::Error> {
391 Ok(ServerCertVerified::assertion())
392 }
393
394 fn verify_tls12_signature(
395 &self,
396 _message: &[u8],
397 _cert: &rustls::pki_types::CertificateDer<'_>,
398 _dss: &rustls::DigitallySignedStruct,
399 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
400 {
401 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
402 }
403
404 fn verify_tls13_signature(
405 &self,
406 _message: &[u8],
407 _cert: &rustls::pki_types::CertificateDer<'_>,
408 _dss: &rustls::DigitallySignedStruct,
409 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
410 {
411 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
412 }
413
414 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
415 vec![
416 rustls::SignatureScheme::RSA_PKCS1_SHA1,
417 rustls::SignatureScheme::ECDSA_SHA1_Legacy,
418 rustls::SignatureScheme::RSA_PKCS1_SHA256,
419 rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
420 rustls::SignatureScheme::RSA_PKCS1_SHA384,
421 rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
422 rustls::SignatureScheme::RSA_PKCS1_SHA512,
423 rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
424 rustls::SignatureScheme::RSA_PSS_SHA256,
425 rustls::SignatureScheme::RSA_PSS_SHA384,
426 rustls::SignatureScheme::RSA_PSS_SHA512,
427 rustls::SignatureScheme::ED25519,
428 rustls::SignatureScheme::ED448,
429 ]
430 }
431 }
432
433 let mut cfg = rustls::ClientConfig::builder()
434 .dangerous()
435 .with_custom_certificate_verifier(Arc::new(Verifier))
436 .with_no_client_auth();
437 cfg.enable_early_data = true;
438
439 quinn::ClientConfig::new(Arc::new(
440 quinn::crypto::rustls::QuicClientConfig::try_from(cfg).unwrap(),
441 ))
442 };
443
444 addr::try_connect(network, &hostname, override_port, prefer_ipv6, |a| {
445 ConnectAddr::Quic(a, config.clone(), hostname.clone())
446 })
447 .await
448}
449
450impl Client {
451 pub async fn new(
452 addr: ConnectionArgs,
453 runtime: Arc<Runtime>,
454 mismatched_server_info: &mut Option<ServerInfo>,
456 username: &str,
457 password: &str,
458 locale: Option<String>,
459 auth_trusted: impl FnMut(&str) -> bool,
460 init_stage_update: &(dyn Fn(ClientInitStage) + Send + Sync),
461 add_foreign_systems: impl Fn(&mut DispatcherBuilder) + Send + 'static,
462 #[cfg_attr(not(feature = "plugins"), expect(unused_variables))] config_dir: PathBuf,
463 client_type: ClientType,
464 ) -> Result<Self, Error> {
465 let _ = rustls::crypto::ring::default_provider().install_default(); let network = Network::new(Pid::new(), &runtime);
467
468 init_stage_update(ClientInitStage::ConnectionEstablish);
469
470 let mut participant = match addr {
471 ConnectionArgs::Srv {
472 hostname,
473 prefer_ipv6,
474 validate_tls,
475 use_quic,
476 } => {
477 let resolver = Resolver::builder_tokio()
481 .unwrap_or_else(|error| {
482 error!(
483 "Failed to create DNS resolver using system configuration: {error:?}"
484 );
485 warn!("Falling back to a default configured resolver.");
486 Resolver::builder_with_config(
487 ResolverConfig::default(),
488 TokioRuntimeProvider::default(),
489 )
490 })
491 .build()
492 .expect(
493 "Could not get a Hickory DNS resolver, maybe you are missing some tls libs",
494 );
495
496 let quic_service_host = format!("_veloren._udp.{hostname}");
497 let quic_lookup_future = resolver.srv_lookup(quic_service_host);
498 let tcp_service_host = format!("_veloren._tcp.{hostname}");
499 let tcp_lookup_future = resolver.srv_lookup(tcp_service_host);
500 let (quic_rr, tcp_rr) = tokio::join!(quic_lookup_future, tcp_lookup_future);
501
502 #[derive(Eq, PartialEq)]
503 enum ConnMode {
504 Quic,
505 Tcp,
506 }
507
508 let mut srv_rr = Vec::new();
511 let () = quic_rr.map_or_else(
512 |error| {
513 warn!("QUIC SRV lookup failed: {error:?}");
514 },
515 |srv_lookup| {
516 srv_rr.extend(srv_lookup.answers().iter().filter_map(|record| {
517 if let RData::SRV(srv) = &record.data {
518 Some((ConnMode::Quic, srv.clone()))
519 } else {
520 None
521 }
522 }))
523 },
524 );
525 let () = tcp_rr.map_or_else(
526 |error| {
527 warn!("TCP SRV lookup failed: {error:?}");
528 },
529 |srv_lookup| {
530 srv_rr.extend(srv_lookup.answers().iter().filter_map(|record| {
531 if let RData::SRV(srv) = &record.data {
532 Some((ConnMode::Tcp, srv.clone()))
533 } else {
534 None
535 }
536 }))
537 },
538 );
539
540 let srv_rr_slice = srv_rr.as_mut_slice();
542 srv_rr_slice.sort_by_key(|(_, srv)| srv.priority);
543
544 let mut iter = srv_rr_slice.iter();
545
546 loop {
548 if let Some((conn_mode, srv_rr)) = iter.next() {
549 let hostname = format!("{}", srv_rr.target);
550 let port = Some(srv_rr.port);
551 let conn_result = match conn_mode {
552 ConnMode::Quic => {
553 connect_quic(&network, hostname, port, prefer_ipv6, validate_tls)
554 .await
555 },
556 ConnMode::Tcp => {
557 addr::try_connect(
558 &network,
559 &hostname,
560 port,
561 prefer_ipv6,
562 ConnectAddr::Tcp,
563 )
564 .await
565 },
566 };
567 match conn_result {
568 Ok(c) => break c,
569 Err(error) => {
570 warn!("Failed to connect to host {}: {error:?}", srv_rr.target)
571 },
572 }
573 } else {
574 warn!(
575 "No SRV hosts succeeded connection, falling back to direct connection"
576 );
577 let c = if use_quic {
580 connect_quic(&network, hostname, None, prefer_ipv6, validate_tls)
581 .await?
582 } else {
583 match addr::try_connect(
584 &network,
585 &hostname,
586 None,
587 prefer_ipv6,
588 ConnectAddr::Tcp,
589 )
590 .await
591 {
592 Ok(c) => c,
593 Err(error) => return Err(error),
594 }
595 };
596 break c;
597 }
598 }
599 },
600 ConnectionArgs::Tcp {
601 hostname,
602 prefer_ipv6,
603 } => {
604 addr::try_connect(&network, &hostname, None, prefer_ipv6, ConnectAddr::Tcp).await?
605 },
606 ConnectionArgs::Quic {
607 hostname,
608 prefer_ipv6,
609 validate_tls,
610 } => {
611 warn!(
612 "QUIC is enabled. This is experimental and you won't be able to connect to \
613 TCP servers unless deactivated"
614 );
615
616 connect_quic(&network, hostname, None, prefer_ipv6, validate_tls).await?
617 },
618 ConnectionArgs::Mpsc(id) => network.connect(ConnectAddr::Mpsc(id)).await?,
619 };
620
621 let stream = participant.opened().await?;
622 let ping_stream = participant.opened().await?;
623 let mut register_stream = participant.opened().await?;
624 let character_screen_stream = participant.opened().await?;
625 let in_game_stream = participant.opened().await?;
626 let terrain_stream = participant.opened().await?;
627
628 init_stage_update(ClientInitStage::WatingForServerVersion);
629 register_stream.send(client_type)?;
630 let server_info: ServerInfo = register_stream.recv().await?;
631 if server_info.git_hash != *common::util::GIT_HASH
632 || server_info.git_timestamp != *common::util::GIT_TIMESTAMP
633 {
634 warn!(
635 "Server is running {}, you are running {}, versions might be incompatible!",
636 common::util::make_display_version(server_info.git_hash, server_info.git_timestamp),
637 *common::util::DISPLAY_VERSION,
638 );
639 }
640 *mismatched_server_info = Some(server_info.clone());
643 debug!("Auth Server: {:?}", server_info.auth_provider);
644
645 ping_stream.send(PingMsg::Ping)?;
646
647 init_stage_update(ClientInitStage::Authentication);
648 Self::register(
650 username,
651 password,
652 locale,
653 auth_trusted,
654 &server_info,
655 &mut register_stream,
656 )
657 .await?;
658
659 init_stage_update(ClientInitStage::LoadingInitData);
660 let mut ping_interval = tokio::time::interval(Duration::from_secs(1));
662 let ServerInit::GameSync {
663 entity_package,
664 time_of_day,
665 max_group_size,
666 client_timeout,
667 world_map,
668 recipe_book,
669 component_recipe_book,
670 material_stats,
671 ability_map,
672 server_constants,
673 description,
674 active_plugins: _active_plugins,
675 role,
676 } = loop {
677 tokio::select! {
678 res = register_stream.recv() => break res?,
681 _ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?,
682 }
683 };
684
685 init_stage_update(ClientInitStage::StartingClient);
686 let mut task = tokio::task::spawn_blocking(move || {
689 let map_size_lg =
690 common::terrain::MapSizeLg::new(world_map.dimensions_lg).map_err(|_| {
691 Error::Other(format!(
692 "Server sent bad world map dimensions: {:?}",
693 world_map.dimensions_lg,
694 ))
695 })?;
696 let sea_level = world_map.default_chunk.get_min_z() as f32;
697
698 let pools = State::pools(GameMode::Client);
700 let mut state = State::client(
701 pools,
702 map_size_lg,
703 world_map.default_chunk,
704 |dispatch_builder| {
706 add_local_systems(dispatch_builder);
707 add_foreign_systems(dispatch_builder);
708 },
709 #[cfg(feature = "plugins")]
710 common_state::plugin::PluginMgr::from_asset_or_default(),
711 );
712
713 #[cfg_attr(not(feature = "plugins"), expect(unused_mut))]
714 let mut missing_plugins: Vec<PluginHash> = Vec::new();
715 #[cfg_attr(not(feature = "plugins"), expect(unused_mut))]
716 let mut local_plugins: Vec<PathBuf> = Vec::new();
717 #[cfg(feature = "plugins")]
718 {
719 let already_present = state.ecs().read_resource::<PluginMgr>().plugin_list();
720 for hash in _active_plugins.iter() {
721 if !already_present.contains(hash) {
722 if let Ok(local_path) = common_state::plugin::find_cached(&config_dir, hash)
724 {
725 local_plugins.push(local_path);
726 } else {
727 tracing::info!("Server requires plugin {hash:x?}");
729 missing_plugins.push(*hash);
730 }
731 }
732 }
733 }
734 state.ecs_mut().register::<comp::Last<CharacterState>>();
736 let entity = state.ecs_mut().apply_entity_package(entity_package);
737 *state.ecs_mut().write_resource() = time_of_day;
738 *state.ecs_mut().write_resource() = PlayerEntity(Some(entity));
739 state.ecs_mut().insert(material_stats);
740 state.ecs_mut().insert(ability_map);
741 state.ecs_mut().insert(recipe_book);
742
743 let map_size = map_size_lg.chunks();
744 let max_height = world_map.max_height;
745 let rgba = world_map.rgba;
746 let alt = world_map.alt;
747 if rgba.size() != map_size.map(|e| e as i32) {
748 return Err(Error::Other("Server sent a bad world map image".into()));
749 }
750 if alt.size() != map_size.map(|e| e as i32) {
751 return Err(Error::Other("Server sent a bad altitude map.".into()));
752 }
753 let [west, east] = world_map.horizons;
754 let scale_angle = |a: u8| (a as f32 / 255.0 * <f32 as FloatConst>::FRAC_PI_2()).tan();
755 let scale_height = |h: u8| h as f32 / 255.0 * max_height;
756 let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height;
757
758 debug!("Preparing image...");
759 let unzip_horizons = |(angles, heights): &(Vec<_>, Vec<_>)| {
760 (
761 angles.iter().copied().map(scale_angle).collect::<Vec<_>>(),
762 heights
763 .iter()
764 .copied()
765 .map(scale_height)
766 .collect::<Vec<_>>(),
767 )
768 };
769 let horizons = [unzip_horizons(&west), unzip_horizons(&east)];
770
771 let mut world_map_rgba = vec![0u32; rgba.size().product() as usize];
773 let mut world_map_topo = vec![0u32; rgba.size().product() as usize];
774 let mut map_config = common::terrain::map::MapConfig::orthographic(
775 map_size_lg,
776 core::ops::RangeInclusive::new(0.0, max_height),
777 );
778 map_config.horizons = Some(&horizons);
779 let rescale_height = |h: f32| h / max_height;
780 let bounds_check = |pos: Vec2<i32>| {
781 pos.reduce_partial_min() >= 0
782 && pos.x < map_size.x as i32
783 && pos.y < map_size.y as i32
784 };
785 fn sample_pos(
786 map_config: &MapConfig,
787 pos: Vec2<i32>,
788 alt: &Grid<u32>,
789 rgba: &Grid<u32>,
790 map_size: &Vec2<u16>,
791 map_size_lg: &common::terrain::MapSizeLg,
792 max_height: f32,
793 ) -> common::terrain::map::MapSample {
794 let rescale_height = |h: f32| h / max_height;
795 let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height;
796 let bounds_check = |pos: Vec2<i32>| {
797 pos.reduce_partial_min() >= 0
798 && pos.x < map_size.x as i32
799 && pos.y < map_size.y as i32
800 };
801 let MapConfig {
802 gain,
803 is_contours,
804 is_height_map,
805 is_stylized_topo,
806 ..
807 } = *map_config;
808 let mut is_contour_line = false;
809 let mut is_border = false;
810 let (rgb, alt, downhill_wpos) = if bounds_check(pos) {
811 let posi = pos.y as usize * map_size.x as usize + pos.x as usize;
812 let [r, g, b, _a] = rgba[pos].to_le_bytes();
813 let is_water = r == 0 && b > 102 && g < 77;
814 let alti = alt[pos];
815 let altj = rescale_height(scale_height_big(alti));
817 let contour_interval = 150.0;
818 let chunk_contour = (altj * gain / contour_interval) as u32;
819
820 let downhill = {
822 let mut best = -1;
823 let mut besth = alti;
824 for nposi in neighbors(*map_size_lg, posi) {
825 let nbh = alt.raw()[nposi];
826 let nalt = rescale_height(scale_height_big(nbh));
827 let nchunk_contour = (nalt * gain / contour_interval) as u32;
828 if !is_contour_line && chunk_contour > nchunk_contour {
829 is_contour_line = true;
830 }
831 let [nr, ng, nb, _na] = rgba.raw()[nposi].to_le_bytes();
832 let n_is_water = nr == 0 && nb > 102 && ng < 77;
833
834 if !is_border && is_water && !n_is_water {
835 is_border = true;
836 }
837
838 if nbh < besth {
839 besth = nbh;
840 best = nposi as isize;
841 }
842 }
843 best
844 };
845 let downhill_wpos = if downhill < 0 {
846 None
847 } else {
848 Some(
849 Vec2::new(
850 (downhill as usize % map_size.x as usize) as i32,
851 (downhill as usize / map_size.x as usize) as i32,
852 ) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
853 )
854 };
855 (Rgb::new(r, g, b), alti, downhill_wpos)
856 } else {
857 (Rgb::zero(), 0, None)
858 };
859 let alt = f64::from(rescale_height(scale_height_big(alt)));
860 let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
861 let downhill_wpos =
862 downhill_wpos.unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32));
863 let is_path = rgb.r == 0x37 && rgb.g == 0x29 && rgb.b == 0x23;
864 let rgb = rgb.map(|e: u8| e as f64 / 255.0);
865 let is_water = rgb.r == 0.0 && rgb.b > 0.4 && rgb.g < 0.3;
866
867 let rgb = if is_height_map {
868 if is_path {
869 Rgb::new(0.9, 0.9, 0.63)
871 } else if is_water {
872 Rgb::new(0.23, 0.47, 0.53)
873 } else if is_contours && is_contour_line {
874 Rgb::new(0.15, 0.15, 0.15)
876 } else {
877 let lightness = (alt + 0.2).min(1.0);
879 Rgb::new(lightness, 0.9 * lightness, 0.5 * lightness)
880 }
881 } else if is_stylized_topo {
882 if is_path {
883 Rgb::new(0.9, 0.9, 0.63)
884 } else if is_water {
885 if is_border {
886 Rgb::new(0.10, 0.34, 0.50)
887 } else {
888 Rgb::new(0.23, 0.47, 0.63)
889 }
890 } else if is_contour_line {
891 Rgb::new(0.25, 0.25, 0.25)
892 } else {
893 Rgb::new(
895 (rgb.r + 0.25).min(1.0),
896 (rgb.g + 0.23).min(1.0),
897 (rgb.b + 0.10).min(1.0),
898 )
899 }
900 } else {
901 Rgb::new(rgb.r, rgb.g, rgb.b)
902 }
903 .map(|e| (e * 255.0) as u8);
904 common::terrain::map::MapSample {
905 rgb,
906 alt,
907 downhill_wpos,
908 connections: None,
909 }
910 }
911 map_config.is_shaded = true;
913 map_config.generate(
914 |pos| {
915 sample_pos(
916 &map_config,
917 pos,
918 &alt,
919 &rgba,
920 &map_size,
921 &map_size_lg,
922 max_height,
923 )
924 },
925 |wpos| {
926 let pos = wpos.wpos_to_cpos();
927 rescale_height(if bounds_check(pos) {
928 scale_height_big(alt[pos])
929 } else {
930 0.0
931 })
932 },
933 |pos, (r, g, b, a)| {
934 world_map_rgba[pos.y * map_size.x as usize + pos.x] =
935 u32::from_le_bytes([r, g, b, a]);
936 },
937 );
938 map_config.is_contours = true;
940 map_config.is_stylized_topo = true;
941 map_config.generate(
942 |pos| {
943 sample_pos(
944 &map_config,
945 pos,
946 &alt,
947 &rgba,
948 &map_size,
949 &map_size_lg,
950 max_height,
951 )
952 },
953 |wpos| {
954 let pos = wpos.wpos_to_cpos();
955 rescale_height(if bounds_check(pos) {
956 scale_height_big(alt[pos])
957 } else {
958 0.0
959 })
960 },
961 |pos, (r, g, b, a)| {
962 world_map_topo[pos.y * map_size.x as usize + pos.x] =
963 u32::from_le_bytes([r, g, b, a]);
964 },
965 );
966 let make_raw = |rgb| -> Result<_, Error> {
967 let mut raw = vec![0u8; 4 * world_map_rgba.len()];
968 LittleEndian::write_u32_into(rgb, &mut raw);
969 Ok(Arc::new(
970 DynamicImage::ImageRgba8({
971 let map =
973 image::ImageBuffer::from_raw(u32::from(map_size.x), u32::from(map_size.y), raw);
974 map.ok_or_else(|| Error::Other("Server sent a bad world map image".into()))?
975 })
976 .flipv(),
979 ))
980 };
981 let lod_base = rgba;
982 let lod_alt = alt;
983 let world_map_rgb_img = make_raw(&world_map_rgba)?;
984 let world_map_topo_img = make_raw(&world_map_topo)?;
985 let world_map_layers = vec![world_map_rgb_img, world_map_topo_img];
986 let horizons = (west.0, west.1, east.0, east.1)
987 .into_par_iter()
988 .map(|(wa, wh, ea, eh)| u32::from_le_bytes([wa, wh, ea, eh]))
989 .collect::<Vec<_>>();
990 let lod_horizon = horizons;
991 let map_bounds = Vec2::new(sea_level, max_height);
992 debug!("Done preparing image...");
993
994 Ok((
995 state,
996 lod_base,
997 lod_alt,
998 Grid::from_raw(map_size.map(|e| e as i32), lod_horizon),
999 (world_map_layers, map_size, map_bounds),
1000 world_map.sites,
1001 world_map.possible_starting_sites,
1002 world_map.pois,
1003 component_recipe_book,
1004 max_group_size,
1005 client_timeout,
1006 missing_plugins,
1007 local_plugins,
1008 role,
1009 ))
1010 });
1011
1012 let (
1013 state,
1014 lod_base,
1015 lod_alt,
1016 lod_horizon,
1017 world_map,
1018 sites,
1019 possible_starting_sites,
1020 pois,
1021 component_recipe_book,
1022 max_group_size,
1023 client_timeout,
1024 missing_plugins,
1025 local_plugins,
1026 role,
1027 ) = loop {
1028 tokio::select! {
1029 res = &mut task => break res.expect("Client thread should not panic")?,
1030 _ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?,
1031 }
1032 };
1033 let missing_plugins_set = missing_plugins.iter().cloned().collect();
1034 if !missing_plugins.is_empty() {
1035 stream.send(ClientGeneral::RequestPlugins(missing_plugins))?;
1036 }
1037 ping_stream.send(PingMsg::Ping)?;
1038
1039 debug!("Initial sync done");
1040
1041 Ok(Self {
1042 client_type,
1043 registered: true,
1044 presence: None,
1045 runtime,
1046 server_info,
1047 server_description: description,
1048 world_data: WorldData {
1049 lod_base,
1050 lod_alt,
1051 lod_horizon,
1052 map: world_map,
1053 },
1054 weather: WeatherLerp::default(),
1055 player_list: HashMap::new(),
1056 character_list: CharacterList::default(),
1057 character_being_deleted: None,
1058 sites: sites
1059 .iter()
1060 .filter_map(|m| {
1061 Some((m.site?, SiteMarker {
1062 marker: m.clone(),
1063 economy: None,
1064 }))
1065 })
1066 .collect(),
1067 extra_markers: sites.iter().filter(|m| m.site.is_none()).cloned().collect(),
1068 possible_starting_sites,
1069 pois,
1070 component_recipe_book,
1071 available_recipes: HashMap::default(),
1072 chat_mode: ChatMode::default(),
1073
1074 lod_zones: HashMap::new(),
1075 lod_last_requested: None,
1076 lod_pos_fallback: None,
1077
1078 force_update_counter: 0,
1079
1080 role,
1081 max_group_size,
1082 invite: None,
1083 group_leader: None,
1084 group_members: HashMap::new(),
1085 pending_invites: HashSet::new(),
1086 pending_trade: None,
1087 waypoint: None,
1088
1089 network: Some(network),
1090 participant: Some(participant),
1091 general_stream: stream,
1092 ping_stream,
1093 register_stream,
1094 character_screen_stream,
1095 in_game_stream,
1096 terrain_stream,
1097
1098 client_timeout,
1099
1100 last_server_ping: 0.0,
1101 last_server_pong: 0.0,
1102 last_ping_delta: 0.0,
1103 ping_deltas: VecDeque::new(),
1104
1105 tick: 0,
1106 state,
1107
1108 flashing_lights_enabled: true,
1109
1110 server_view_distance_limit: None,
1111 view_distance: None,
1112 lod_distance: 4.0,
1113 loaded_distance: 0.0,
1114
1115 pending_chunks: HashMap::new(),
1116 target_time_of_day: None,
1117 dt_adjustment: 1.0,
1118
1119 connected_server_constants: server_constants,
1120 missing_plugins: missing_plugins_set,
1121 local_plugins,
1122 })
1123 }
1124
1125 async fn register(
1127 username: &str,
1128 password: &str,
1129 locale: Option<String>,
1130 mut auth_trusted: impl FnMut(&str) -> bool,
1131 server_info: &ServerInfo,
1132 register_stream: &mut Stream,
1133 ) -> Result<(), Error> {
1134 let token_or_username = match &server_info.auth_provider {
1136 Some(addr) => {
1137 if auth_trusted(addr) {
1139 let (scheme, authority) = match addr.split_once("://") {
1140 Some((s, a)) => (s, a),
1141 None => return Err(Error::AuthServerUrlInvalid(addr.to_string())),
1142 };
1143
1144 let scheme = match scheme.parse::<authc::Scheme>() {
1145 Ok(s) => s,
1146 Err(_) => return Err(Error::AuthServerUrlInvalid(addr.to_string())),
1147 };
1148
1149 let authority = match authority.parse::<authc::Authority>() {
1150 Ok(a) => a,
1151 Err(_) => return Err(Error::AuthServerUrlInvalid(addr.to_string())),
1152 };
1153
1154 Ok(authc::AuthClient::new(scheme, authority)?
1155 .sign_in(username, password)
1156 .await?
1157 .serialize())
1158 } else {
1159 Err(Error::AuthServerNotTrusted)
1160 }
1161 },
1162 None => Ok(username.to_owned()),
1163 }?;
1164
1165 debug!("Registering client...");
1166
1167 register_stream.send(ClientRegister {
1168 token_or_username,
1169 locale,
1170 })?;
1171
1172 match register_stream.recv::<ServerRegisterAnswer>().await? {
1173 Err(RegisterError::AuthError(err)) => Err(Error::AuthErr(err)),
1174 Err(RegisterError::InvalidCharacter) => Err(Error::InvalidCharacter),
1175 Err(RegisterError::NotOnWhitelist) => Err(Error::NotOnWhitelist),
1176 Err(RegisterError::Kicked(err)) => Err(Error::Kicked(err)),
1177 Err(RegisterError::Banned(info)) => Err(Error::Banned(info)),
1178 Err(RegisterError::TooManyPlayers) => Err(Error::TooManyPlayers),
1179 Ok(()) => {
1180 debug!("Client registered successfully.");
1181 Ok(())
1182 },
1183 }
1184 }
1185
1186 fn send_msg_err<S>(&mut self, msg: S) -> Result<(), network::StreamError>
1187 where
1188 S: Into<ClientMsg>,
1189 {
1190 prof_span!("send_msg_err");
1191 let msg: ClientMsg = msg.into();
1192 #[cfg(debug_assertions)]
1193 {
1194 const C_TYPE: ClientType = ClientType::Game;
1195 let verified = msg.verify(C_TYPE, self.registered, self.presence);
1196
1197 if !verified {
1201 warn!(
1202 "Received ClientType::Game message when not in game (Registered: {} Presence: \
1203 {:?}), dropping message: {:?} ",
1204 self.registered, self.presence, msg
1205 );
1206 return Ok(());
1207 }
1208 }
1209 match msg {
1210 ClientMsg::Type(msg) => self.register_stream.send(msg),
1211 ClientMsg::Register(msg) => self.register_stream.send(msg),
1212 ClientMsg::General(msg) => {
1213 #[cfg(feature = "tracy")]
1214 let (mut ingame, mut terrain) = (0.0, 0.0);
1215 let stream = match msg {
1216 ClientGeneral::RequestCharacterList
1217 | ClientGeneral::CreateCharacter { .. }
1218 | ClientGeneral::EditCharacter { .. }
1219 | ClientGeneral::DeleteCharacter(_)
1220 | ClientGeneral::Character(_, _)
1221 | ClientGeneral::Spectate(_) => &mut self.character_screen_stream,
1222 ClientGeneral::ControllerInputs(_)
1224 | ClientGeneral::ControlEvent(_)
1225 | ClientGeneral::ControlAction(_)
1226 | ClientGeneral::SetViewDistance(_)
1227 | ClientGeneral::BreakBlock(_)
1228 | ClientGeneral::PlaceBlock(_, _)
1229 | ClientGeneral::ExitInGame
1230 | ClientGeneral::PlayerPhysics { .. }
1231 | ClientGeneral::UnlockSkill(_)
1232 | ClientGeneral::RequestSiteInfo(_)
1233 | ClientGeneral::RequestPlayerPhysics { .. }
1234 | ClientGeneral::RequestLossyTerrainCompression { .. }
1235 | ClientGeneral::UpdateMapMarker(_)
1236 | ClientGeneral::SpectatePosition(_)
1237 | ClientGeneral::SpectateEntity(_)
1238 | ClientGeneral::SetBattleMode(_) => {
1239 #[cfg(feature = "tracy")]
1240 {
1241 ingame = 1.0;
1242 }
1243 &mut self.in_game_stream
1244 },
1245 ClientGeneral::TerrainChunkRequest { .. }
1247 | ClientGeneral::LodZoneRequest { .. } => {
1248 #[cfg(feature = "tracy")]
1249 {
1250 terrain = 1.0;
1251 }
1252 &mut self.terrain_stream
1253 },
1254 ClientGeneral::ChatMsg(_)
1256 | ClientGeneral::Command(_, _)
1257 | ClientGeneral::Terminate
1258 | ClientGeneral::RequestPlugins(_) => &mut self.general_stream,
1259 };
1260 #[cfg(feature = "tracy")]
1261 {
1262 plot!("ingame_sends", ingame);
1263 plot!("terrain_sends", terrain);
1264 }
1265 stream.send(msg)
1266 },
1267 ClientMsg::Ping(msg) => self.ping_stream.send(msg),
1268 }
1269 }
1270
1271 pub fn request_player_physics(&mut self, server_authoritative: bool) {
1272 self.send_msg(ClientGeneral::RequestPlayerPhysics {
1273 server_authoritative,
1274 })
1275 }
1276
1277 pub fn request_lossy_terrain_compression(&mut self, lossy_terrain_compression: bool) {
1278 self.send_msg(ClientGeneral::RequestLossyTerrainCompression {
1279 lossy_terrain_compression,
1280 })
1281 }
1282
1283 fn send_msg<S>(&mut self, msg: S)
1284 where
1285 S: Into<ClientMsg>,
1286 {
1287 let res = self.send_msg_err(msg);
1288 if let Err(e) = res {
1289 warn!(
1290 ?e,
1291 "connection to server no longer possible, couldn't send msg"
1292 );
1293 }
1294 }
1295
1296 pub fn request_character(
1298 &mut self,
1299 character_id: CharacterId,
1300 view_distances: common::ViewDistances,
1301 ) {
1302 let view_distances = self.set_view_distances_local(view_distances);
1303 self.send_msg(ClientGeneral::Character(character_id, view_distances));
1304
1305 if let Some(character) = self
1306 .character_list
1307 .characters
1308 .iter()
1309 .find(|x| x.character.id == Some(character_id))
1310 {
1311 self.waypoint = character.location.clone();
1312 }
1313
1314 self.presence = Some(PresenceKind::Character(character_id));
1316 }
1317
1318 pub fn request_spectate(&mut self, view_distances: common::ViewDistances) {
1320 let view_distances = self.set_view_distances_local(view_distances);
1321 self.send_msg(ClientGeneral::Spectate(view_distances));
1322
1323 self.presence = Some(PresenceKind::Spectator);
1324 }
1325
1326 pub fn load_character_list(&mut self) {
1328 self.character_list.loading = true;
1329 self.send_msg(ClientGeneral::RequestCharacterList);
1330 }
1331
1332 pub fn create_character(
1334 &mut self,
1335 alias: String,
1336 mainhand: Option<String>,
1337 offhand: Option<String>,
1338 body: comp::Body,
1339 hardcore: bool,
1340 start_site: Option<SiteId>,
1341 ) {
1342 self.character_list.loading = true;
1343 self.send_msg(ClientGeneral::CreateCharacter {
1344 alias,
1345 mainhand,
1346 offhand,
1347 body,
1348 hardcore,
1349 start_site,
1350 });
1351 }
1352
1353 pub fn edit_character(&mut self, alias: String, id: CharacterId, body: comp::Body) {
1354 self.character_list.loading = true;
1355 self.send_msg(ClientGeneral::EditCharacter { alias, id, body });
1356 }
1357
1358 pub fn delete_character(&mut self, character_id: CharacterId) {
1360 if let Some(pos) = self
1364 .character_list
1365 .characters
1366 .iter()
1367 .position(|x| x.character.id == Some(character_id))
1368 {
1369 self.character_list.characters.remove(pos);
1370 }
1371 self.send_msg(ClientGeneral::DeleteCharacter(character_id));
1372 }
1373
1374 pub fn logout(&mut self) {
1376 debug!("Sending logout from server");
1377 self.send_msg(ClientGeneral::Terminate);
1378 self.registered = false;
1379 self.presence = None;
1380 }
1381
1382 pub fn request_remove_character(&mut self) {
1385 self.chat_mode = ChatMode::World;
1386 self.send_msg(ClientGeneral::ExitInGame);
1387 }
1388
1389 pub fn set_view_distances(&mut self, view_distances: common::ViewDistances) {
1390 let view_distances = self.set_view_distances_local(view_distances);
1391 self.send_msg(ClientGeneral::SetViewDistance(view_distances));
1392 }
1393
1394 fn set_view_distances_local(
1398 &mut self,
1399 view_distances: common::ViewDistances,
1400 ) -> common::ViewDistances {
1401 let view_distances = common::ViewDistances {
1402 terrain: view_distances
1403 .terrain
1404 .clamp(1, MAX_SELECTABLE_VIEW_DISTANCE),
1405 entity: view_distances.entity.max(1),
1406 };
1407 self.view_distance = Some(view_distances.terrain);
1408 view_distances
1409 }
1410
1411 pub fn set_lod_distance(&mut self, lod_distance: u32) {
1412 let lod_distance = lod_distance.clamp(0, 1000) as f32 / lod::ZONE_SIZE as f32;
1413 self.lod_distance = lod_distance;
1414 }
1415
1416 pub fn set_flashing_lights_enabled(&mut self, flashing_lights_enabled: bool) {
1417 self.flashing_lights_enabled = flashing_lights_enabled;
1418 }
1419
1420 pub fn use_slot(&mut self, slot: Slot) {
1421 self.control_action(ControlAction::InventoryAction(InventoryAction::Use(slot)))
1422 }
1423
1424 pub fn swap_slots(&mut self, a: Slot, b: Slot) {
1425 match (a, b) {
1426 (Slot::Overflow(o), Slot::Inventory(inv))
1427 | (Slot::Inventory(inv), Slot::Overflow(o)) => {
1428 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1429 InventoryEvent::OverflowMove(o, inv),
1430 )));
1431 },
1432 (Slot::Overflow(_), _) | (_, Slot::Overflow(_)) => {},
1433 (Slot::Equip(equip), slot) | (slot, Slot::Equip(equip)) => self.control_action(
1434 ControlAction::InventoryAction(InventoryAction::Swap(equip, slot)),
1435 ),
1436 (Slot::Inventory(inv1), Slot::Inventory(inv2)) => {
1437 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1438 InventoryEvent::Swap(inv1, inv2),
1439 )))
1440 },
1441 }
1442 }
1443
1444 pub fn drop_slot(&mut self, slot: Slot) {
1445 match slot {
1446 Slot::Equip(equip) => {
1447 self.control_action(ControlAction::InventoryAction(InventoryAction::Drop(equip)))
1448 },
1449 Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent(
1450 ControlEvent::InventoryEvent(InventoryEvent::Drop(inv)),
1451 )),
1452 Slot::Overflow(o) => self.send_msg(ClientGeneral::ControlEvent(
1453 ControlEvent::InventoryEvent(InventoryEvent::OverflowDrop(o)),
1454 )),
1455 }
1456 }
1457
1458 pub fn sort_inventory(&mut self, sort_order: InventorySortOrder) {
1459 self.control_action(ControlAction::InventoryAction(InventoryAction::Sort(
1460 sort_order,
1461 )));
1462 }
1463
1464 pub fn perform_trade_action(&mut self, action: TradeAction) {
1465 if let Some((id, _, _)) = self.pending_trade {
1466 if let TradeAction::Decline = action {
1467 self.pending_trade.take();
1468 }
1469 self.send_msg(ClientGeneral::ControlEvent(
1470 ControlEvent::PerformTradeAction(id, action),
1471 ));
1472 }
1473 }
1474
1475 pub fn is_dead(&self) -> bool { self.current::<comp::Health>().is_some_and(|h| h.is_dead) }
1476
1477 pub fn is_gliding(&self) -> bool {
1478 self.current::<CharacterState>()
1479 .is_some_and(|cs| matches!(cs, CharacterState::Glide(_)))
1480 }
1481
1482 pub fn split_swap_slots(&mut self, a: Slot, b: Slot) {
1483 match (a, b) {
1484 (Slot::Overflow(_), _) | (_, Slot::Overflow(_)) => {},
1485 (Slot::Equip(equip), slot) | (slot, Slot::Equip(equip)) => self.control_action(
1486 ControlAction::InventoryAction(InventoryAction::Swap(equip, slot)),
1487 ),
1488 (Slot::Inventory(inv1), Slot::Inventory(inv2)) => {
1489 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1490 InventoryEvent::SplitSwap(inv1, inv2),
1491 )))
1492 },
1493 }
1494 }
1495
1496 pub fn split_drop_slot(&mut self, slot: Slot) {
1497 match slot {
1498 Slot::Equip(equip) => {
1499 self.control_action(ControlAction::InventoryAction(InventoryAction::Drop(equip)))
1500 },
1501 Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent(
1502 ControlEvent::InventoryEvent(InventoryEvent::SplitDrop(inv)),
1503 )),
1504 Slot::Overflow(o) => self.send_msg(ClientGeneral::ControlEvent(
1505 ControlEvent::InventoryEvent(InventoryEvent::OverflowSplitDrop(o)),
1506 )),
1507 }
1508 }
1509
1510 pub fn pick_up(&mut self, entity: EcsEntity) {
1511 if let Some(uid) = self.state.read_component_copied(entity) {
1514 if self.is_dead() {
1516 return;
1517 }
1518
1519 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1520 InventoryEvent::Pickup(uid),
1521 )));
1522 }
1523 }
1524
1525 pub fn do_pet(&mut self, target_entity: EcsEntity) {
1526 if self.is_dead() {
1527 return;
1528 }
1529
1530 if let Some(target_uid) = self.state.read_component_copied(target_entity) {
1531 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InteractWith {
1532 target: target_uid,
1533 kind: common::interaction::InteractionKind::Pet,
1534 }))
1535 }
1536 }
1537
1538 pub fn npc_interact(&mut self, npc_entity: EcsEntity) {
1539 if self.is_dead() {
1541 return;
1542 }
1543
1544 if let Some(uid) = self.state.read_component_copied(npc_entity) {
1545 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Interact(uid)));
1546 }
1547 }
1548
1549 pub fn player_list(&self) -> &HashMap<Uid, PlayerInfo> { &self.player_list }
1550
1551 pub fn character_list(&self) -> &CharacterList { &self.character_list }
1552
1553 pub fn server_info(&self) -> &ServerInfo { &self.server_info }
1554
1555 pub fn server_description(&self) -> &ServerDescription { &self.server_description }
1556
1557 pub fn world_data(&self) -> &WorldData { &self.world_data }
1558
1559 pub fn component_recipe_book(&self) -> &ComponentRecipeBook { &self.component_recipe_book }
1560
1561 pub fn client_type(&self) -> &ClientType { &self.client_type }
1562
1563 pub fn available_recipes(&self) -> &HashMap<String, Option<SpriteKind>> {
1564 &self.available_recipes
1565 }
1566
1567 pub fn lod_zones(&self) -> &HashMap<Vec2<i32>, lod::Zone> { &self.lod_zones }
1568
1569 pub fn set_lod_pos_fallback(&mut self, pos: Vec2<f32>) { self.lod_pos_fallback = Some(pos); }
1572
1573 pub fn craft_recipe(
1574 &mut self,
1575 recipe: &str,
1576 slots: Vec<(u32, InvSlotId)>,
1577 craft_sprite: Option<(VolumePos, SpriteKind)>,
1578 amount: u32,
1579 ) -> bool {
1580 let (can_craft, has_sprite) = if let Some(inventory) = self
1581 .state
1582 .ecs()
1583 .read_storage::<comp::Inventory>()
1584 .get(self.entity())
1585 {
1586 let rbm = self.state.ecs().read_resource::<RecipeBookManifest>();
1587 let (can_craft, required_sprite) = inventory.can_craft_recipe(recipe, 1, &rbm);
1588 let has_sprite =
1589 required_sprite.is_none_or(|s| Some(s) == craft_sprite.map(|(_, s)| s));
1590 (can_craft, has_sprite)
1591 } else {
1592 (false, false)
1593 };
1594 if can_craft && has_sprite {
1595 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1596 InventoryEvent::CraftRecipe {
1597 craft_event: CraftEvent::Simple {
1598 recipe: recipe.to_string(),
1599 slots,
1600 amount,
1601 },
1602 craft_sprite: craft_sprite.map(|(pos, _)| pos),
1603 },
1604 )));
1605 true
1606 } else {
1607 false
1608 }
1609 }
1610
1611 pub fn can_salvage_item(&self, slot: InvSlotId) -> bool {
1613 self.inventories()
1614 .get(self.entity())
1615 .and_then(|inv| inv.get(slot))
1616 .is_some_and(|item| item.is_salvageable())
1617 }
1618
1619 pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: VolumePos) -> bool {
1622 let is_salvageable = self.can_salvage_item(slot);
1623 if is_salvageable {
1624 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1625 InventoryEvent::CraftRecipe {
1626 craft_event: CraftEvent::Salvage(slot),
1627 craft_sprite: Some(salvage_pos),
1628 },
1629 )));
1630 }
1631 is_salvageable
1632 }
1633
1634 pub fn craft_modular_weapon(
1640 &mut self,
1641 primary_component: InvSlotId,
1642 secondary_component: InvSlotId,
1643 sprite_pos: Option<VolumePos>,
1644 ) -> bool {
1645 let inventories = self.inventories();
1646 let inventory = inventories.get(self.entity());
1647
1648 enum ModKind {
1649 Primary,
1650 Secondary,
1651 }
1652
1653 let mod_kind = |slot| match inventory
1655 .and_then(|inv| inv.get(slot).map(|item| item.kind()))
1656 .as_deref()
1657 {
1658 Some(ItemKind::ModularComponent(modular::ModularComponent::ToolPrimaryComponent {
1659 ..
1660 })) => Some(ModKind::Primary),
1661 Some(ItemKind::ModularComponent(
1662 modular::ModularComponent::ToolSecondaryComponent { .. },
1663 )) => Some(ModKind::Secondary),
1664 _ => None,
1665 };
1666
1667 if let (Some(ModKind::Primary), Some(ModKind::Secondary)) =
1668 (mod_kind(primary_component), mod_kind(secondary_component))
1669 {
1670 drop(inventories);
1671 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1672 InventoryEvent::CraftRecipe {
1673 craft_event: CraftEvent::ModularWeapon {
1674 primary_component,
1675 secondary_component,
1676 },
1677 craft_sprite: sprite_pos,
1678 },
1679 )));
1680 true
1681 } else {
1682 false
1683 }
1684 }
1685
1686 pub fn craft_modular_weapon_component(
1687 &mut self,
1688 toolkind: tool::ToolKind,
1689 material: InvSlotId,
1690 modifier: Option<InvSlotId>,
1691 slots: Vec<(u32, InvSlotId)>,
1692 sprite_pos: Option<VolumePos>,
1693 ) {
1694 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1695 InventoryEvent::CraftRecipe {
1696 craft_event: CraftEvent::ModularWeaponPrimaryComponent {
1697 toolkind,
1698 material,
1699 modifier,
1700 slots,
1701 },
1702 craft_sprite: sprite_pos,
1703 },
1704 )));
1705 }
1706
1707 pub fn repair_item(&mut self, item: Slot, sprite_pos: VolumePos) -> bool {
1710 let is_repairable = {
1711 let inventories = self.inventories();
1712 let inventory = inventories.get(self.entity());
1713 inventory.is_some_and(|inv| {
1714 if let Some(item) = match item {
1715 Slot::Equip(equip_slot) => inv.equipped(equip_slot),
1716 Slot::Inventory(invslot) => inv.get(invslot),
1717 Slot::Overflow(_) => None,
1718 } {
1719 item.has_durability()
1720 } else {
1721 false
1722 }
1723 })
1724 };
1725 if is_repairable {
1726 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1727 InventoryEvent::CraftRecipe {
1728 craft_event: CraftEvent::Repair(item),
1729 craft_sprite: Some(sprite_pos),
1730 },
1731 )));
1732 }
1733 is_repairable
1734 }
1735
1736 fn update_available_recipes(&mut self) {
1737 let rbm = self.state.ecs().read_resource::<RecipeBookManifest>();
1738 let inventories = self.state.ecs().read_storage::<comp::Inventory>();
1739 if let Some(inventory) = inventories.get(self.entity()) {
1740 self.available_recipes = inventory
1741 .recipes_iter()
1742 .cloned()
1743 .filter_map(|name| {
1744 let (can_craft, required_sprite) = inventory.can_craft_recipe(&name, 1, &rbm);
1745 if can_craft {
1746 Some((name, required_sprite))
1747 } else {
1748 None
1749 }
1750 })
1751 .collect();
1752 }
1753 }
1754
1755 pub fn sites(&self) -> &HashMap<SiteId, SiteMarker> { &self.sites }
1757
1758 pub fn markers(&self) -> impl Iterator<Item = &Marker> {
1759 self.sites
1760 .values()
1761 .map(|s| &s.marker)
1762 .chain(self.extra_markers.iter())
1763 }
1764
1765 pub fn possible_starting_sites(&self) -> &[SiteId] { &self.possible_starting_sites }
1766
1767 pub fn pois(&self) -> &Vec<PoiInfo> { &self.pois }
1769
1770 pub fn enable_lantern(&mut self) {
1771 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::EnableLantern));
1772 }
1773
1774 pub fn disable_lantern(&mut self) {
1775 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::DisableLantern));
1776 }
1777
1778 pub fn toggle_sprite_light(&mut self, pos: VolumePos, enable: bool) {
1779 self.control_action(ControlAction::InventoryAction(
1780 InventoryAction::ToggleSpriteLight(pos, enable),
1781 ));
1782 }
1783
1784 pub fn help_downed(&mut self, target_entity: EcsEntity) {
1785 if self.is_dead() {
1786 return;
1787 }
1788
1789 if let Some(target_uid) = self.state.read_component_copied(target_entity) {
1790 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InteractWith {
1791 target: target_uid,
1792 kind: common::interaction::InteractionKind::HelpDowned,
1793 }))
1794 }
1795 }
1796
1797 pub fn remove_buff(&mut self, buff_id: BuffKind) {
1798 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::RemoveBuff(
1799 buff_id,
1800 )));
1801 }
1802
1803 pub fn leave_stance(&mut self) {
1804 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::LeaveStance));
1805 }
1806
1807 pub fn unlock_skill(&mut self, skill: Skill) {
1808 self.send_msg(ClientGeneral::UnlockSkill(skill));
1809 }
1810
1811 pub fn max_group_size(&self) -> u32 { self.max_group_size }
1812
1813 pub fn invite(&self) -> Option<(Uid, Instant, Duration, InviteKind)> { self.invite }
1814
1815 pub fn group_info(&self) -> Option<(String, Uid)> {
1816 self.group_leader.map(|l| ("Group".into(), l)) }
1818
1819 pub fn group_members(&self) -> &HashMap<Uid, group::Role> { &self.group_members }
1820
1821 pub fn pending_invites(&self) -> &HashSet<Uid> { &self.pending_invites }
1822
1823 pub fn pending_trade(&self) -> &Option<(TradeId, PendingTrade, Option<SitePrices>)> {
1824 &self.pending_trade
1825 }
1826
1827 pub fn is_trading(&self) -> bool { self.pending_trade.is_some() }
1828
1829 pub fn send_invite(&mut self, invitee: Uid, kind: InviteKind) {
1830 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateInvite(
1831 invitee, kind,
1832 )))
1833 }
1834
1835 pub fn accept_invite(&mut self) {
1836 self.invite.take();
1838 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InviteResponse(
1839 InviteResponse::Accept,
1840 )));
1841 }
1842
1843 pub fn decline_invite(&mut self) {
1844 self.invite.take();
1846 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InviteResponse(
1847 InviteResponse::Decline,
1848 )));
1849 }
1850
1851 pub fn leave_group(&mut self) {
1852 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
1853 GroupManip::Leave,
1854 )));
1855 }
1856
1857 pub fn kick_from_group(&mut self, uid: Uid) {
1858 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
1859 GroupManip::Kick(uid),
1860 )));
1861 }
1862
1863 pub fn assign_group_leader(&mut self, uid: Uid) {
1864 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
1865 GroupManip::AssignLeader(uid),
1866 )));
1867 }
1868
1869 pub fn is_riding(&self) -> bool {
1870 self.state
1871 .ecs()
1872 .read_storage::<Is<Rider>>()
1873 .get(self.entity())
1874 .is_some()
1875 || self
1876 .state
1877 .ecs()
1878 .read_storage::<Is<VolumeRider>>()
1879 .get(self.entity())
1880 .is_some()
1881 }
1882
1883 pub fn is_lantern_enabled(&self) -> bool {
1884 self.state
1885 .ecs()
1886 .read_storage::<comp::LightEmitter>()
1887 .get(self.entity())
1888 .is_some()
1889 }
1890
1891 pub fn mount(&mut self, entity: EcsEntity) {
1892 if let Some(uid) = self.state.read_component_copied(entity) {
1893 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Mount(uid)));
1894 }
1895 }
1896
1897 pub fn mount_volume(&mut self, volume_pos: VolumePos) {
1899 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::MountVolume(
1900 volume_pos,
1901 )));
1902 }
1903
1904 pub fn unmount(&mut self) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Unmount)); }
1905
1906 pub fn set_pet_stay(&mut self, entity: EcsEntity, stay: bool) {
1907 if let Some(uid) = self.state.read_component_copied(entity) {
1908 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::SetPetStay(
1909 uid, stay,
1910 )));
1911 }
1912 }
1913
1914 pub fn give_up(&mut self) {
1915 if comp::is_downed(self.current().as_ref(), self.current().as_ref()) {
1916 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GiveUp));
1917 }
1918 }
1919
1920 pub fn respawn(&mut self) -> bool {
1921 if self.current::<comp::Health>().is_some_and(|h| h.is_dead) {
1922 if self.current::<Hardcore>().is_some() {
1924 self.request_remove_character();
1925 } else {
1926 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Respawn));
1927 }
1928 true
1929 } else {
1930 false
1931 }
1932 }
1933
1934 pub fn map_marker_event(&mut self, event: MapMarkerChange) {
1935 self.send_msg(ClientGeneral::UpdateMapMarker(event));
1936 }
1937
1938 pub fn spectate_position(&mut self, pos: Vec3<f32>) -> bool {
1941 let write = if let Some(position) = self
1942 .state
1943 .ecs()
1944 .write_storage::<comp::Pos>()
1945 .get_mut(self.entity())
1946 {
1947 position.0 = pos;
1948 true
1949 } else {
1950 false
1951 };
1952 if write {
1953 self.send_msg(ClientGeneral::SpectatePosition(pos));
1954 }
1955 write
1956 }
1957
1958 pub fn start_spectate_entity(&mut self, entity: EcsEntity) {
1959 if let Some(uid) = self.state.read_component_copied(entity) {
1960 self.send_msg(ClientGeneral::SpectateEntity(Some(uid)));
1961 } else {
1962 warn!("Spectating entity without a `Uid` component");
1963 }
1964 }
1965
1966 pub fn stop_spectate_entity(&mut self) { self.send_msg(ClientGeneral::SpectateEntity(None)); }
1967
1968 pub fn swap_loadout(&mut self) { self.control_action(ControlAction::SwapEquippedWeapons) }
1971
1972 pub fn is_wielding(&self) -> Option<bool> {
1975 self.state
1976 .ecs()
1977 .read_storage::<CharacterState>()
1978 .get(self.entity())
1979 .map(|cs| cs.is_wield())
1980 }
1981
1982 pub fn toggle_wield(&mut self) {
1983 match self.is_wielding() {
1984 Some(true) => self.control_action(ControlAction::Unwield),
1985 Some(false) => self.control_action(ControlAction::Wield),
1986 None => warn!("Can't toggle wield, client entity doesn't have a `CharacterState`"),
1987 }
1988 }
1989
1990 pub fn toggle_sit(&mut self) {
1991 let is_sitting = self
1992 .state
1993 .ecs()
1994 .read_storage::<CharacterState>()
1995 .get(self.entity())
1996 .map(|cs| matches!(cs, CharacterState::Sit));
1997
1998 match is_sitting {
1999 Some(true) => self.control_action(ControlAction::Stand),
2000 Some(false) => self.control_action(ControlAction::Sit),
2001 None => warn!("Can't toggle sit, client entity doesn't have a `CharacterState`"),
2002 }
2003 }
2004
2005 pub fn toggle_crawl(&mut self) {
2006 let is_crawling = self
2007 .state
2008 .ecs()
2009 .read_storage::<CharacterState>()
2010 .get(self.entity())
2011 .map(|cs| matches!(cs, CharacterState::Crawl));
2012
2013 match is_crawling {
2014 Some(true) => self.control_action(ControlAction::Stand),
2015 Some(false) => self.control_action(ControlAction::Crawl),
2016 None => warn!("Can't toggle crawl, client entity doesn't have a `CharacterState`"),
2017 }
2018 }
2019
2020 pub fn toggle_dance(&mut self) {
2021 let is_dancing = self
2022 .state
2023 .ecs()
2024 .read_storage::<CharacterState>()
2025 .get(self.entity())
2026 .map(|cs| matches!(cs, CharacterState::Dance));
2027
2028 match is_dancing {
2029 Some(true) => self.control_action(ControlAction::Stand),
2030 Some(false) => self.control_action(ControlAction::Dance),
2031 None => warn!("Can't toggle dance, client entity doesn't have a `CharacterState`"),
2032 }
2033 }
2034
2035 pub fn utter(&mut self, kind: UtteranceKind) {
2036 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Utterance(kind)));
2037 }
2038
2039 pub fn toggle_sneak(&mut self) {
2040 let is_sneaking = self
2041 .state
2042 .ecs()
2043 .read_storage::<CharacterState>()
2044 .get(self.entity())
2045 .map(CharacterState::is_stealthy);
2046
2047 match is_sneaking {
2048 Some(true) => self.control_action(ControlAction::Stand),
2049 Some(false) => self.control_action(ControlAction::Sneak),
2050 None => warn!("Can't toggle sneak, client entity doesn't have a `CharacterState`"),
2051 }
2052 }
2053
2054 pub fn toggle_glide(&mut self) {
2055 let using_glider = self
2056 .state
2057 .ecs()
2058 .read_storage::<CharacterState>()
2059 .get(self.entity())
2060 .map(|cs| matches!(cs, CharacterState::GlideWield(_) | CharacterState::Glide(_)));
2061
2062 match using_glider {
2063 Some(true) => self.control_action(ControlAction::Unwield),
2064 Some(false) => self.control_action(ControlAction::GlideWield),
2065 None => warn!("Can't toggle glide, client entity doesn't have a `CharacterState`"),
2066 }
2067 }
2068
2069 pub fn cancel_climb(&mut self) {
2070 let is_climbing = self
2071 .state
2072 .ecs()
2073 .read_storage::<CharacterState>()
2074 .get(self.entity())
2075 .map(|cs| matches!(cs, CharacterState::Climb(_)));
2076
2077 match is_climbing {
2078 Some(true) => self.control_action(ControlAction::Stand),
2079 Some(false) => {},
2080 None => warn!("Can't stop climbing, client entity doesn't have a `CharacterState`"),
2081 }
2082 }
2083
2084 pub fn handle_input(
2085 &mut self,
2086 input: InputKind,
2087 pressed: bool,
2088 select_pos: Option<Vec3<f32>>,
2089 target_entity: Option<EcsEntity>,
2090 ) {
2091 if pressed {
2092 self.control_action(ControlAction::StartInput {
2093 input,
2094 target_entity: target_entity.and_then(|e| self.state.read_component_copied(e)),
2095 select_pos,
2096 });
2097 } else {
2098 self.control_action(ControlAction::CancelInput { input });
2099 }
2100 }
2101
2102 pub fn activate_portal(&mut self, portal: EcsEntity) {
2103 if let Some(portal_uid) = self.state.read_component_copied(portal) {
2104 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::ActivatePortal(
2105 portal_uid,
2106 )));
2107 }
2108 }
2109
2110 fn control_action(&mut self, control_action: ControlAction) {
2111 if let Some(controller) = self
2112 .state
2113 .ecs()
2114 .write_storage::<Controller>()
2115 .get_mut(self.entity())
2116 {
2117 controller.push_action(control_action);
2118 }
2119 self.send_msg(ClientGeneral::ControlAction(control_action));
2120 }
2121
2122 fn control_event(&mut self, control_event: ControlEvent) {
2123 if let Some(controller) = self
2124 .state
2125 .ecs()
2126 .write_storage::<Controller>()
2127 .get_mut(self.entity())
2128 {
2129 controller.push_event(control_event.clone());
2130 }
2131 self.send_msg(ClientGeneral::ControlEvent(control_event));
2132 }
2133
2134 pub fn view_distance(&self) -> Option<u32> { self.view_distance }
2135
2136 pub fn server_view_distance_limit(&self) -> Option<u32> { self.server_view_distance_limit }
2137
2138 pub fn loaded_distance(&self) -> f32 { self.loaded_distance }
2139
2140 pub fn position(&self) -> Option<Vec3<f32>> {
2141 self.state
2142 .read_storage::<comp::Pos>()
2143 .get(self.entity())
2144 .map(|v| v.0)
2145 }
2146
2147 pub fn weather_at_player(&self) -> Weather {
2149 self.position()
2150 .map(|p| {
2151 let mut weather = self.state.weather_at(p.xy());
2152 weather.wind = self.weather.local_wind;
2153 weather
2154 })
2155 .unwrap_or_default()
2156 }
2157
2158 pub fn current_chunk(&self) -> Option<Arc<TerrainChunk>> {
2159 let chunk_pos = Vec2::from(self.position()?)
2160 .map2(TerrainChunkSize::RECT_SIZE, |e: f32, sz| {
2161 (e as u32).div_euclid(sz) as i32
2162 });
2163
2164 self.state.terrain().get_key_arc(chunk_pos).cloned()
2165 }
2166
2167 pub fn chunks_around(&self, radius: i32) -> Option<Vec<(Arc<TerrainChunk>, Vec2<i32>)>> {
2170 let chunk_pos = Vec2::from(self.position()?)
2171 .map2(TerrainChunkSize::RECT_SIZE, |e: f32, sz| {
2172 (e as u32).div_euclid(sz) as i32
2173 });
2174
2175 Some(
2176 Spiral2d::with_radius(radius)
2177 .filter_map(|coord| {
2178 let pos = chunk_pos + coord;
2179 self.state
2180 .terrain()
2181 .get_key_arc(pos)
2182 .map(|chunk| (Arc::clone(chunk), pos))
2183 })
2184 .collect(),
2185 )
2186 }
2187
2188 pub fn current<C>(&self) -> Option<C>
2189 where
2190 C: Component + Clone,
2191 {
2192 self.state.read_storage::<C>().get(self.entity()).cloned()
2193 }
2194
2195 pub fn current_biome(&self) -> BiomeKind {
2196 match self.current_chunk() {
2197 Some(chunk) => chunk.meta().biome(),
2198 _ => BiomeKind::Void,
2199 }
2200 }
2201
2202 pub fn current_site(&self) -> SiteKindMeta {
2203 let mut player_alt = 0.0;
2204 if let Some(position) = self.current::<comp::Pos>() {
2205 player_alt = position.0.z;
2206 }
2207 let mut terrain_alt = 0.0;
2208 let mut site = None;
2209 if let Some(chunk) = self.current_chunk() {
2210 terrain_alt = chunk.meta().alt();
2211 site = chunk.meta().site();
2212 }
2213 if player_alt < terrain_alt - 40.0 {
2214 if let Some(SiteKindMeta::Dungeon(dungeon)) = site {
2215 SiteKindMeta::Dungeon(dungeon)
2216 } else {
2217 SiteKindMeta::Cave
2218 }
2219 } else {
2220 site.unwrap_or_default()
2221 }
2222 }
2223
2224 pub fn request_site_economy(&mut self, id: SiteId) {
2225 self.send_msg(ClientGeneral::RequestSiteInfo(id))
2226 }
2227
2228 pub fn inventories(&self) -> ReadStorage<'_, comp::Inventory> { self.state.read_storage() }
2229
2230 pub fn send_chat(&mut self, message: String) {
2232 self.send_msg(ClientGeneral::ChatMsg(comp::Content::Plain(message)));
2233 }
2234
2235 pub fn send_command(&mut self, name: String, args: Vec<String>) {
2237 self.send_msg(ClientGeneral::Command(name, args));
2238 }
2239
2240 pub fn clear_terrain(&mut self) {
2242 self.state.clear_terrain();
2243 self.pending_chunks.clear();
2244 }
2245
2246 pub fn place_block(&mut self, pos: Vec3<i32>, block: Block) {
2247 self.send_msg(ClientGeneral::PlaceBlock(pos, block));
2248 }
2249
2250 pub fn remove_block(&mut self, pos: Vec3<i32>) {
2251 self.send_msg(ClientGeneral::BreakBlock(pos));
2252 }
2253
2254 pub fn collect_block(&mut self, pos: Vec3<i32>) {
2255 self.control_action(ControlAction::InventoryAction(InventoryAction::Collect(
2256 pos,
2257 )));
2258 }
2259
2260 pub fn perform_dialogue(&mut self, target: EcsEntity, dialogue: rtsim::Dialogue) {
2261 if let Some(target_uid) = self.state.read_component_copied(target) {
2262 self.control_event(ControlEvent::Dialogue(target_uid, dialogue));
2267 }
2268 }
2269
2270 pub fn do_talk(&mut self, tgt: Option<EcsEntity>) {
2271 if let Some(controller) = self
2272 .state
2273 .ecs()
2274 .write_storage::<comp::Controller>()
2275 .get_mut(self.entity())
2276 {
2277 controller.push_action(ControlAction::Talk(
2278 tgt.and_then(|tgt| self.state.read_component_copied(tgt)),
2279 ));
2280 }
2281 }
2282
2283 pub fn change_ability(&mut self, slot: usize, new_ability: comp::ability::AuxiliaryAbility) {
2284 let auxiliary_key = self
2285 .inventories()
2286 .get(self.entity())
2287 .map_or((None, None), |inv| {
2288 let tool_kind = |slot| {
2289 inv.equipped(slot).and_then(|item| match &*item.kind() {
2290 ItemKind::Tool(tool) => Some(tool.kind),
2291 _ => None,
2292 })
2293 };
2294
2295 (
2296 tool_kind(EquipSlot::ActiveMainhand),
2297 tool_kind(EquipSlot::ActiveOffhand),
2298 )
2299 });
2300
2301 self.send_msg(ClientGeneral::ControlEvent(ControlEvent::ChangeAbility {
2302 slot,
2303 auxiliary_key,
2304 new_ability,
2305 }))
2306 }
2307
2308 pub fn waypoint(&self) -> &Option<String> { &self.waypoint }
2309
2310 pub fn set_battle_mode(&mut self, battle_mode: BattleMode) {
2311 self.send_msg(ClientGeneral::SetBattleMode(battle_mode));
2312 }
2313
2314 pub fn get_battle_mode(&self) -> BattleMode {
2315 let Some(uid) = self.uid() else {
2316 error!("Client entity does not have a Uid component");
2317
2318 return BattleMode::PvP;
2319 };
2320
2321 let Some(player_info) = self.player_list.get(&uid) else {
2322 error!("Client does not have PlayerInfo for its Uid");
2323
2324 return BattleMode::PvP;
2325 };
2326
2327 let Some(ref character_info) = player_info.character else {
2328 error!("Client does not have CharacterInfo for its PlayerInfo");
2329
2330 return BattleMode::PvP;
2331 };
2332
2333 character_info.battle_mode
2334 }
2335
2336 pub fn tick(&mut self, inputs: ControllerInputs, dt: Duration) -> Result<Vec<Event>, Error> {
2339 span!(_guard, "tick", "Client::tick");
2340 if self.presence.is_some() {
2360 prof_span!("handle and send inputs");
2361 if let Err(e) = self
2362 .state
2363 .ecs()
2364 .write_storage::<Controller>()
2365 .entry(self.entity())
2366 .map(|entry| {
2367 entry
2368 .or_insert_with(|| Controller {
2369 inputs: inputs.clone(),
2370 queued_inputs: BTreeMap::new(),
2371 events: Vec::new(),
2372 actions: Vec::new(),
2373 })
2374 .inputs = inputs.clone();
2375 })
2376 {
2377 let entry = self.entity();
2378 error!(
2379 ?e,
2380 ?entry,
2381 "Couldn't access controller component on client entity"
2382 );
2383 }
2384 self.send_msg_err(ClientGeneral::ControllerInputs(Box::new(inputs)))?;
2385 }
2386
2387 let mut frontend_events = Vec::new();
2389
2390 {
2392 prof_span!("Last<CharacterState> comps update");
2393 let ecs = self.state.ecs();
2394 let mut last_character_states = ecs.write_storage::<comp::Last<CharacterState>>();
2395 for (entity, _, character_state) in (
2396 &ecs.entities(),
2397 &ecs.read_storage::<comp::Body>(),
2398 &ecs.read_storage::<CharacterState>(),
2399 )
2400 .join()
2401 {
2402 if let Some(l) = last_character_states
2403 .entry(entity)
2404 .ok()
2405 .map(|l| l.or_insert_with(|| comp::Last(character_state.clone())))
2406 .filter(|l| !character_state.same_variant(&l.0))
2409 {
2410 *l = comp::Last(character_state.clone());
2411 }
2412 }
2413 }
2414
2415 frontend_events.append(&mut self.handle_new_messages()?);
2417
2418 if self
2421 .invite
2422 .is_some_and(|(_, timeout, dur, _)| timeout.elapsed() > dur)
2423 {
2424 self.invite = None;
2425 }
2426
2427 self.weather.update(&mut self.state.weather_grid_mut());
2429
2430 if let Some(target_tod) = self.target_time_of_day {
2431 let mut tod = self.state.ecs_mut().write_resource::<TimeOfDay>();
2432 tod.0 = target_tod.0;
2433 self.target_time_of_day = None;
2434 }
2435
2436 if self.current::<Hardcore>().is_some()
2439 && self.is_dead()
2440 && let Some(PresenceKind::Character(character_id)) = self.presence
2441 {
2442 self.character_being_deleted = Some(character_id);
2443 }
2444
2445 self.state.tick(
2447 Duration::from_secs_f64(dt.as_secs_f64() * self.dt_adjustment),
2448 true,
2449 None,
2450 &self.connected_server_constants,
2451 |_, _| {},
2452 );
2453
2454 let _ = self.state.ecs().fetch::<EventBus<Outcome>>().recv_all();
2461
2462 self.tick_terrain()?;
2464
2465 if self.state.get_program_time() - self.last_server_ping > 1. {
2467 self.send_msg_err(PingMsg::Ping)?;
2468 self.last_server_ping = self.state.get_program_time();
2469 }
2470
2471 if self.presence.is_some()
2473 && let (Some(pos), Some(vel), Some(ori)) = (
2474 self.state.read_storage().get(self.entity()).cloned(),
2475 self.state.read_storage().get(self.entity()).cloned(),
2476 self.state.read_storage().get(self.entity()).cloned(),
2477 )
2478 {
2479 self.in_game_stream.send(ClientGeneral::PlayerPhysics {
2480 pos,
2481 vel,
2482 ori,
2483 force_counter: self.force_update_counter,
2484 })?;
2485 }
2486
2487 self.tick += 1;
2501 Ok(frontend_events)
2502 }
2503
2504 pub fn cleanup(&mut self) {
2506 self.state.cleanup();
2508 }
2509
2510 fn tick_terrain(&mut self) -> Result<(), Error> {
2515 let pos = self
2516 .state
2517 .read_storage::<comp::Pos>()
2518 .get(self.entity())
2519 .cloned();
2520 if let (Some(pos), Some(view_distance)) = (pos, self.view_distance) {
2521 prof_span!("terrain");
2522 let chunk_pos = self.state.terrain().pos_key(pos.0.map(|e| e as i32));
2523
2524 let mut chunks_to_remove = Vec::new();
2526 self.state.terrain().iter().for_each(|(key, _)| {
2527 if (chunk_pos - key)
2535 .map(|e: i32| (e.unsigned_abs()).saturating_sub(2).min(view_distance + 1))
2536 .magnitude_squared()
2537 > view_distance.pow(2)
2538 {
2539 chunks_to_remove.push(key);
2540 }
2541 });
2542 for key in chunks_to_remove {
2543 self.state.remove_chunk(key);
2544 }
2545
2546 let mut current_tick_send_chunk_requests = 0;
2547 self.loaded_distance = ((view_distance * TerrainChunkSize::RECT_SIZE.x) as f32).powi(2);
2549 for dist in 0..view_distance as i32 + 1 {
2551 let top = if 2 * (dist - 2).max(0).pow(2) > (view_distance - 1).pow(2) as i32 {
2560 ((view_distance - 1).pow(2) as f32 - (dist - 2).pow(2) as f32)
2561 .sqrt()
2562 .round() as i32
2563 + 1
2564 } else {
2565 dist
2566 };
2567
2568 let mut skip_mode = false;
2569 for i in -top..top + 1 {
2570 let keys = [
2571 chunk_pos + Vec2::new(dist, i),
2572 chunk_pos + Vec2::new(i, dist),
2573 chunk_pos + Vec2::new(-dist, i),
2574 chunk_pos + Vec2::new(i, -dist),
2575 ];
2576
2577 for key in keys.iter() {
2578 let dist_to_player = (TerrainGrid::key_chunk(*key).map(|x| x as f32)
2579 + TerrainChunkSize::RECT_SIZE.map(|x| x as f32) / 2.0)
2580 .distance_squared(pos.0.into());
2581
2582 let terrain = self.state.terrain();
2583 if let Some(chunk) = terrain.get_key_arc(*key) {
2584 if !skip_mode && !terrain.contains_key_real(*key) {
2585 let chunk = Arc::clone(chunk);
2586 drop(terrain);
2587 self.state.insert_chunk(*key, chunk);
2588 }
2589 } else {
2590 drop(terrain);
2591 if !skip_mode && !self.pending_chunks.contains_key(key) {
2592 const TOTAL_PENDING_CHUNKS_LIMIT: usize = 12;
2593 const CURRENT_TICK_PENDING_CHUNKS_LIMIT: usize = 2;
2594 if self.pending_chunks.len() < TOTAL_PENDING_CHUNKS_LIMIT
2595 && current_tick_send_chunk_requests
2596 < CURRENT_TICK_PENDING_CHUNKS_LIMIT
2597 {
2598 self.send_msg_err(ClientGeneral::TerrainChunkRequest {
2599 key: *key,
2600 })?;
2601 current_tick_send_chunk_requests += 1;
2602 self.pending_chunks.insert(*key, Instant::now());
2603 } else {
2604 skip_mode = true;
2605 }
2606 }
2607
2608 if dist_to_player < self.loaded_distance {
2609 self.loaded_distance = dist_to_player;
2610 }
2611 }
2612 }
2613 }
2614 }
2615 self.loaded_distance = self.loaded_distance.sqrt()
2616 - ((TerrainChunkSize::RECT_SIZE.x as f32 / 2.0).powi(2)
2617 + (TerrainChunkSize::RECT_SIZE.y as f32 / 2.0).powi(2))
2618 .sqrt();
2619
2620 let now = Instant::now();
2622 self.pending_chunks
2623 .retain(|_, created| now.duration_since(*created) < Duration::from_secs(3));
2624 }
2625
2626 if let Some(lod_pos) = pos.map(|p| p.0.xy()).or(self.lod_pos_fallback) {
2627 let lod_zone = lod_pos.map(|e| lod::from_wpos(e as i32));
2629
2630 if self
2632 .lod_last_requested
2633 .is_none_or(|i| i.elapsed() > Duration::from_secs(5))
2634 && let Some(rpos) = Spiral2d::new()
2635 .take((1 + self.lod_distance.ceil() as i32 * 2).pow(2) as usize)
2636 .filter(|rpos| !self.lod_zones.contains_key(&(lod_zone + *rpos)))
2637 .min_by_key(|rpos| rpos.magnitude_squared())
2638 .filter(|rpos| {
2639 rpos.map(|e| e as f32).magnitude() < (self.lod_distance - 0.5).max(0.0)
2640 })
2641 {
2642 self.send_msg_err(ClientGeneral::LodZoneRequest {
2643 key: lod_zone + rpos,
2644 })?;
2645 self.lod_last_requested = Some(Instant::now());
2646 }
2647
2648 self.lod_zones.retain(|p, _| {
2650 (*p - lod_zone).map(|e| e as f32).magnitude_squared() < self.lod_distance.powi(2)
2651 });
2652 }
2653
2654 Ok(())
2655 }
2656
2657 fn handle_server_msg(
2658 &mut self,
2659 frontend_events: &mut Vec<Event>,
2660 msg: ServerGeneral,
2661 ) -> Result<(), Error> {
2662 prof_span!("handle_server_msg");
2663 match msg {
2664 ServerGeneral::Disconnect(reason) => match reason {
2665 DisconnectReason::Shutdown => return Err(Error::ServerShutdown),
2666 DisconnectReason::Kicked(reason) => return Err(Error::Kicked(reason)),
2667 DisconnectReason::Banned(info) => return Err(Error::Banned(info)),
2668 },
2669 ServerGeneral::PlayerListUpdate(PlayerListUpdate::Init(list)) => {
2670 self.player_list = list
2671 },
2672 ServerGeneral::PlayerListUpdate(PlayerListUpdate::Add(uid, player_info)) => {
2673 if let Some(old_player_info) = self.player_list.insert(uid, player_info.clone()) {
2674 warn!(
2675 "Received msg to insert {} with uid {} into the player list but there was \
2676 already an entry for {} with the same uid that was overwritten!",
2677 player_info.player_alias, uid, old_player_info.player_alias
2678 );
2679 }
2680 },
2681 ServerGeneral::PlayerListUpdate(PlayerListUpdate::Moderator(uid, moderator)) => {
2682 if let Some(player_info) = self.player_list.get_mut(&uid) {
2683 player_info.is_moderator = moderator;
2684 } else {
2685 warn!(
2686 "Received msg to update admin status of uid {}, but they were not in the \
2687 list.",
2688 uid
2689 );
2690 }
2691 },
2692 ServerGeneral::PlayerListUpdate(PlayerListUpdate::SelectedCharacter(
2693 uid,
2694 char_info,
2695 )) => {
2696 if let Some(player_info) = self.player_list.get_mut(&uid) {
2697 player_info.character = Some(char_info);
2698 } else {
2699 warn!(
2700 "Received msg to update character info for uid {}, but they were not in \
2701 the list.",
2702 uid
2703 );
2704 }
2705 },
2706 ServerGeneral::PlayerListUpdate(PlayerListUpdate::ExitCharacter(uid)) => {
2707 if let Some(player_info) = self.player_list.get_mut(&uid) {
2708 if player_info.character.is_none() {
2709 debug!(?player_info.player_alias, ?uid, "Received PlayerListUpdate::ExitCharacter for a player who wasnt ingame");
2710 }
2711 player_info.character = None;
2712 } else {
2713 debug!(
2714 ?uid,
2715 "Received PlayerListUpdate::ExitCharacter for a nonexitent player"
2716 );
2717 }
2718 },
2719 ServerGeneral::PlayerListUpdate(PlayerListUpdate::Remove(uid)) => {
2720 if let Some(player_info) = self.player_list.get_mut(&uid) {
2731 if player_info.is_online {
2732 player_info.is_online = false;
2733 } else {
2734 warn!(
2735 "Received msg to remove uid {} from the player list by they were \
2736 already marked offline",
2737 uid
2738 );
2739 }
2740 } else {
2741 warn!(
2742 "Received msg to remove uid {} from the player list by they weren't in \
2743 the list!",
2744 uid
2745 );
2746 }
2747 },
2748 ServerGeneral::PlayerListUpdate(PlayerListUpdate::Alias(uid, new_name)) => {
2749 if let Some(player_info) = self.player_list.get_mut(&uid) {
2750 player_info.player_alias = new_name;
2751 } else {
2752 warn!(
2753 "Received msg to alias player with uid {} to {} but this uid is not in \
2754 the player list",
2755 uid, new_name
2756 );
2757 }
2758 },
2759 ServerGeneral::PlayerListUpdate(PlayerListUpdate::UpdateBattleMode(
2760 uid,
2761 battle_mode,
2762 )) => {
2763 if let Some(player_info) = self.player_list.get_mut(&uid) {
2764 if let Some(ref mut character_info) = player_info.character {
2765 character_info.battle_mode = battle_mode;
2766 } else {
2767 warn!(
2768 "Received msg to update battle mode of uid {} to {:?} but this player \
2769 does not have a character",
2770 uid, battle_mode
2771 );
2772 }
2773 } else {
2774 warn!(
2775 "Received msg to update battle mode of uid {} to {:?} but this uid is not \
2776 in the player list",
2777 uid, battle_mode
2778 );
2779 }
2780 },
2781 ServerGeneral::ChatMsg(m) => frontend_events.push(Event::Chat(m)),
2782 ServerGeneral::ChatMode(m) => {
2783 self.chat_mode = m;
2784 },
2785 ServerGeneral::SetPlayerEntity(uid) => {
2786 if let Some(entity) = self.state.ecs().entity_from_uid(uid) {
2787 let old_player_entity = mem::replace(
2788 &mut *self.state.ecs_mut().write_resource(),
2789 PlayerEntity(Some(entity)),
2790 );
2791 if let Some(old_entity) = old_player_entity.0 {
2792 let mut controllers = self.state.ecs().write_storage::<Controller>();
2794 if let Some(controller) = controllers.remove(old_entity)
2795 && let Err(e) = controllers.insert(entity, controller)
2796 {
2797 error!(
2798 ?e,
2799 "Failed to insert controller when setting new player entity!"
2800 );
2801 }
2802 }
2803 if let Some(presence) = self.presence {
2804 self.presence = Some(match presence {
2805 PresenceKind::Spectator => PresenceKind::Spectator,
2806 PresenceKind::LoadingCharacter(_) => PresenceKind::Possessor,
2807 PresenceKind::Character(_) => PresenceKind::Possessor,
2808 PresenceKind::Possessor => PresenceKind::Possessor,
2809 });
2810 }
2811 self.pending_trade = None;
2813 } else {
2814 return Err(Error::Other("Failed to find entity from uid.".into()));
2815 }
2816 },
2817 ServerGeneral::TimeOfDay(time_of_day, calendar, new_time, time_scale) => {
2818 self.target_time_of_day = Some(time_of_day);
2819 *self.state.ecs_mut().write_resource() = calendar;
2820 *self.state.ecs_mut().write_resource() = time_scale;
2821 let mut time = self.state.ecs_mut().write_resource::<Time>();
2822 self.dt_adjustment = if new_time.0 > time.0 + 5.0 {
2827 *time = new_time;
2828 1.0
2829 } else if new_time.0 > time.0 {
2830 1.01
2831 } else {
2832 0.99
2833 };
2834 },
2835 ServerGeneral::EntitySync(entity_sync_package) => {
2836 let uid = self.uid();
2837 self.state
2838 .ecs_mut()
2839 .apply_entity_sync_package(entity_sync_package, uid);
2840 },
2841 ServerGeneral::CompSync(comp_sync_package, force_counter) => {
2842 self.force_update_counter = force_counter;
2843 self.state
2844 .ecs_mut()
2845 .apply_comp_sync_package(comp_sync_package);
2846 },
2847 ServerGeneral::CreateEntity(entity_package) => {
2848 self.state.ecs_mut().apply_entity_package(entity_package);
2849 },
2850 ServerGeneral::DeleteEntity(entity_uid) => {
2851 if self.uid() != Some(entity_uid) {
2852 self.state
2853 .ecs_mut()
2854 .delete_entity_and_clear_uid_mapping(entity_uid);
2855 }
2856 },
2857 ServerGeneral::Notification(n) => {
2858 let Notification::WaypointSaved { location_name } = n.clone();
2859 self.waypoint = Some(location_name);
2860
2861 frontend_events.push(Event::Notification(UserNotification::WaypointUpdated));
2862 },
2863 ServerGeneral::PluginData(d) => {
2864 let plugin_len = d.len();
2865 tracing::info!(?plugin_len, "plugin data");
2866 frontend_events.push(Event::PluginDataReceived(d));
2867 },
2868 ServerGeneral::SetPlayerRole(role) => {
2869 debug!(?role, "Updating client role");
2870 self.role = role;
2871 },
2872 _ => unreachable!("Not a general msg"),
2873 }
2874 Ok(())
2875 }
2876
2877 fn handle_server_in_game_msg(
2878 &mut self,
2879 frontend_events: &mut Vec<Event>,
2880 msg: ServerGeneral,
2881 ) -> Result<(), Error> {
2882 prof_span!("handle_server_in_game_msg");
2883 match msg {
2884 ServerGeneral::GroupUpdate(change_notification) => {
2885 use comp::group::ChangeNotification::*;
2886 match change_notification {
2889 Added(uid, role) => {
2890 if !matches!(role, group::Role::Pet)
2893 && !self
2894 .group_members
2895 .values()
2896 .any(|r| !matches!(r, group::Role::Pet))
2897 {
2898 frontend_events
2899 .push(Event::Chat(comp::ChatType::Meta.into_plain_msg(
2901 "Type /g or /group to chat with your group members",
2902 )));
2903 }
2904 if let Some(player_info) = self.player_list.get(&uid) {
2905 frontend_events.push(Event::Chat(
2906 #[expect(deprecated, reason = "i18n alias")]
2908 comp::ChatType::GroupMeta("Group".into()).into_plain_msg(format!(
2909 "[{}] joined group",
2910 self.personalize_alias(uid, player_info.player_alias.clone())
2911 )),
2912 ));
2913 }
2914 if self.group_members.insert(uid, role) == Some(role) {
2915 warn!(
2916 "Received msg to add uid {} to the group members but they were \
2917 already there",
2918 uid
2919 );
2920 }
2921 },
2922 Removed(uid) => {
2923 if let Some(player_info) = self.player_list.get(&uid) {
2924 frontend_events.push(Event::Chat(
2925 #[expect(deprecated, reason = "i18n alias")]
2927 comp::ChatType::GroupMeta("Group".into()).into_plain_msg(format!(
2928 "[{}] left group",
2929 self.personalize_alias(uid, player_info.player_alias.clone())
2930 )),
2931 ));
2932 frontend_events.push(Event::MapMarker(
2933 comp::MapMarkerUpdate::GroupMember(uid, MapMarkerChange::Remove),
2934 ));
2935 }
2936 if self.group_members.remove(&uid).is_none() {
2937 warn!(
2938 "Received msg to remove uid {} from group members but by they \
2939 weren't in there!",
2940 uid
2941 );
2942 }
2943 },
2944 NewLeader(leader) => {
2945 self.group_leader = Some(leader);
2946 },
2947 NewGroup { leader, members } => {
2948 self.group_leader = Some(leader);
2949 self.group_members = members.into_iter().collect();
2950 if let Some(uid) = self.uid() {
2955 self.group_members.remove(&uid);
2956 }
2957 frontend_events.push(Event::MapMarker(comp::MapMarkerUpdate::ClearGroup));
2958 },
2959 NoGroup => {
2960 self.group_leader = None;
2961 self.group_members = HashMap::new();
2962 frontend_events.push(Event::MapMarker(comp::MapMarkerUpdate::ClearGroup));
2963 },
2964 }
2965 },
2966 ServerGeneral::Invite {
2967 inviter,
2968 timeout,
2969 kind,
2970 } => {
2971 self.invite = Some((inviter, Instant::now(), timeout, kind));
2972 },
2973 ServerGeneral::InvitePending(uid) => {
2974 if !self.pending_invites.insert(uid) {
2975 warn!("Received message about pending invite that was already pending");
2976 }
2977 },
2978 ServerGeneral::InviteComplete {
2979 target,
2980 answer,
2981 kind,
2982 } => {
2983 if !self.pending_invites.remove(&target) {
2984 warn!(
2985 "Received completed invite message for invite that was not in the list of \
2986 pending invites"
2987 )
2988 }
2989 frontend_events.push(Event::InviteComplete {
2990 target,
2991 answer,
2992 kind,
2993 });
2994 },
2995 ServerGeneral::GroupInventoryUpdate(item, uid) => {
2996 frontend_events.push(Event::GroupInventoryUpdate(item, uid));
2997 },
2998 ServerGeneral::ExitInGameSuccess => {
3000 self.presence = None;
3001 self.clean_state();
3002 },
3003 ServerGeneral::InventoryUpdate(inventory, events) => {
3004 let mut update_inventory = false;
3005 for event in events.iter() {
3006 match event {
3007 InventoryUpdateEvent::BlockCollectFailed { .. } => {},
3008 InventoryUpdateEvent::EntityCollectFailed { .. } => {},
3009 _ => update_inventory = true,
3010 }
3011 }
3012 if update_inventory {
3013 let entity = self.entity();
3018 if let Err(e) = self
3019 .state
3020 .ecs_mut()
3021 .write_storage()
3022 .insert(entity, inventory)
3023 {
3024 warn!(
3025 ?e,
3026 "Received an inventory update event for client entity, but this \
3027 entity was not found... this may be a bug."
3028 );
3029 }
3030 }
3031
3032 self.update_available_recipes();
3033
3034 frontend_events.push(Event::InventoryUpdated(events));
3035 },
3036 ServerGeneral::Dialogue(sender, dialogue) => {
3037 frontend_events.push(Event::Dialogue(sender, dialogue));
3038 },
3039 ServerGeneral::SetViewDistance(vd) => {
3040 self.view_distance = Some(vd);
3041 frontend_events.push(Event::SetViewDistance(vd));
3042 self.server_view_distance_limit = Some(vd);
3045 },
3046 ServerGeneral::Outcomes(outcomes) => {
3047 frontend_events.extend(outcomes.into_iter().map(Event::Outcome))
3048 },
3049 ServerGeneral::Knockback(impulse) => {
3050 self.state
3051 .ecs()
3052 .read_resource::<EventBus<LocalEvent>>()
3053 .emit_now(LocalEvent::ApplyImpulse {
3054 entity: self.entity(),
3055 impulse,
3056 });
3057 },
3058 ServerGeneral::UpdatePendingTrade(id, trade, pricing) => {
3059 trace!("UpdatePendingTrade {:?} {:?}", id, trade);
3060 self.pending_trade = Some((id, trade, pricing));
3061 },
3062 ServerGeneral::FinishedTrade(result) => {
3063 if let Some((_, trade, _)) = self.pending_trade.take() {
3064 frontend_events.push(Event::TradeComplete { result, trade })
3065 }
3066 },
3067 ServerGeneral::SiteEconomy(economy) => {
3068 if let Some(rich) = self.sites.get_mut(&economy.id) {
3069 rich.economy = Some(economy);
3070 }
3071 },
3072 ServerGeneral::MapMarker(event) => {
3073 frontend_events.push(Event::MapMarker(event));
3074 },
3075 ServerGeneral::WeatherUpdate(weather) => {
3076 self.weather.weather_update(weather);
3077 },
3078 ServerGeneral::LocalWindUpdate(wind) => {
3079 self.weather.local_wind_update(wind);
3080 },
3081 ServerGeneral::SpectatePosition(pos) => {
3082 frontend_events.push(Event::SpectatePosition(pos));
3083 },
3084 ServerGeneral::UpdateRecipes => {
3085 self.update_available_recipes();
3086 },
3087 ServerGeneral::Gizmos(gizmos) => frontend_events.push(Event::Gizmos(gizmos)),
3088 _ => unreachable!("Not a in_game message"),
3089 }
3090 Ok(())
3091 }
3092
3093 fn handle_server_terrain_msg(&mut self, msg: ServerGeneral) -> Result<(), Error> {
3094 prof_span!("handle_server_terrain_mgs");
3095 match msg {
3096 ServerGeneral::TerrainChunkUpdate { key, chunk } => {
3097 if let Some(chunk) = chunk.ok().and_then(|c| c.to_chunk()) {
3098 self.state.insert_chunk(key, Arc::new(chunk));
3099 }
3100 self.pending_chunks.remove(&key);
3101 },
3102 ServerGeneral::LodZoneUpdate { key, zone } => {
3103 self.lod_zones.insert(key, zone);
3104 self.lod_last_requested = None;
3105 },
3106 ServerGeneral::TerrainBlockUpdates(blocks) => {
3107 if let Some(mut blocks) = blocks.decompress() {
3108 blocks.drain().for_each(|(pos, block)| {
3109 self.state.set_block(pos, block);
3110 });
3111 }
3112 },
3113 _ => unreachable!("Not a terrain message"),
3114 }
3115 Ok(())
3116 }
3117
3118 fn handle_server_character_screen_msg(
3119 &mut self,
3120 events: &mut Vec<Event>,
3121 msg: ServerGeneral,
3122 ) -> Result<(), Error> {
3123 prof_span!("handle_server_character_screen_msg");
3124 match msg {
3125 ServerGeneral::CharacterListUpdate(character_list) => {
3126 self.character_list.characters = character_list;
3127 if self.character_being_deleted.is_some() {
3128 if let Some(pos) = self
3129 .character_list
3130 .characters
3131 .iter()
3132 .position(|x| x.character.id == self.character_being_deleted)
3133 {
3134 self.character_list.characters.remove(pos);
3135 } else {
3136 self.character_being_deleted = None;
3137 }
3138 }
3139 self.character_list.loading = false;
3140 },
3141 ServerGeneral::CharacterActionError(error) => {
3142 warn!("CharacterActionError: {:?}.", error);
3143 events.push(Event::CharacterError(error));
3144 },
3145 ServerGeneral::CharacterDataLoadResult(Ok(metadata)) => {
3146 trace!("Handling join result by server");
3147 events.push(Event::CharacterJoined(metadata));
3148 },
3149 ServerGeneral::CharacterDataLoadResult(Err(error)) => {
3150 trace!("Handling join error by server");
3151 self.presence = None;
3152 self.clean_state();
3153 events.push(Event::CharacterError(error));
3154 },
3155 ServerGeneral::CharacterCreated(character_id) => {
3156 events.push(Event::CharacterCreated(character_id));
3157 },
3158 ServerGeneral::CharacterEdited(character_id) => {
3159 events.push(Event::CharacterEdited(character_id));
3160 },
3161 ServerGeneral::CharacterSuccess => debug!("client is now in ingame state on server"),
3162 ServerGeneral::SpectatorSuccess(spawn_point) => {
3163 events.push(Event::StartSpectate(spawn_point));
3164 debug!("client is now in ingame state on server");
3165 },
3166 _ => unreachable!("Not a character_screen msg"),
3167 }
3168 Ok(())
3169 }
3170
3171 fn handle_ping_msg(&mut self, msg: PingMsg) -> Result<(), Error> {
3172 prof_span!("handle_ping_msg");
3173 match msg {
3174 PingMsg::Ping => {
3175 self.send_msg_err(PingMsg::Pong)?;
3176 },
3177 PingMsg::Pong => {
3178 self.last_server_pong = self.state.get_program_time();
3179 self.last_ping_delta = self.state.get_program_time() - self.last_server_ping;
3180
3181 while self.ping_deltas.len() > PING_ROLLING_AVERAGE_SECS - 1 {
3185 self.ping_deltas.pop_front();
3186 }
3187 self.ping_deltas.push_back(self.last_ping_delta);
3188 },
3189 }
3190 Ok(())
3191 }
3192
3193 fn handle_messages(&mut self, frontend_events: &mut Vec<Event>) -> Result<u64, Error> {
3194 let mut cnt = 0;
3195 #[cfg(feature = "tracy")]
3196 let (mut terrain_cnt, mut ingame_cnt) = (0, 0);
3197 loop {
3198 let cnt_start = cnt;
3199
3200 while let Some(msg) = self.general_stream.try_recv()? {
3201 cnt += 1;
3202 self.handle_server_msg(frontend_events, msg)?;
3203 }
3204 while let Some(msg) = self.ping_stream.try_recv()? {
3205 cnt += 1;
3206 self.handle_ping_msg(msg)?;
3207 }
3208 while let Some(msg) = self.character_screen_stream.try_recv()? {
3209 cnt += 1;
3210 self.handle_server_character_screen_msg(frontend_events, msg)?;
3211 }
3212 while let Some(msg) = self.in_game_stream.try_recv()? {
3213 cnt += 1;
3214 #[cfg(feature = "tracy")]
3215 {
3216 ingame_cnt += 1;
3217 }
3218 self.handle_server_in_game_msg(frontend_events, msg)?;
3219 }
3220 while let Some(msg) = self.terrain_stream.try_recv()? {
3221 cnt += 1;
3222 #[cfg(feature = "tracy")]
3223 {
3224 if let ServerGeneral::TerrainChunkUpdate { chunk, .. } = &msg {
3225 terrain_cnt += chunk.as_ref().map(|x| x.approx_len()).unwrap_or(0);
3226 }
3227 }
3228 self.handle_server_terrain_msg(msg)?;
3229 }
3230
3231 if cnt_start == cnt {
3232 #[cfg(feature = "tracy")]
3233 {
3234 plot!("terrain_recvs", terrain_cnt as f64);
3235 plot!("ingame_recvs", ingame_cnt as f64);
3236 }
3237 return Ok(cnt);
3238 }
3239 }
3240 }
3241
3242 fn handle_new_messages(&mut self) -> Result<Vec<Event>, Error> {
3244 prof_span!("handle_new_messages");
3245 let mut frontend_events = Vec::new();
3246
3247 if self.state.get_program_time() - self.last_server_ping > 1. {
3251 let duration_since_last_pong = self.state.get_program_time() - self.last_server_pong;
3252
3253 const KICK_WARNING_AFTER_REL_TO_TIMEOUT_FRACTION: f64 = 0.75;
3255 if duration_since_last_pong
3256 >= (self.client_timeout.as_secs() as f64
3257 * KICK_WARNING_AFTER_REL_TO_TIMEOUT_FRACTION)
3258 && self.state.get_program_time() - duration_since_last_pong > 0.
3259 {
3260 frontend_events.push(Event::DisconnectionNotification(
3261 (self.state.get_program_time() - duration_since_last_pong).round() as u64,
3262 ));
3263 }
3264 }
3265
3266 let msg_count = self.handle_messages(&mut frontend_events)?;
3267
3268 if msg_count == 0
3269 && self.state.get_program_time() - self.last_server_pong
3270 > self.client_timeout.as_secs() as f64
3271 {
3272 return Err(Error::ServerTimeout);
3273 }
3274
3275 while let Some(res) = self
3277 .participant
3278 .as_mut()
3279 .and_then(|p| p.try_fetch_event().transpose())
3280 {
3281 let event = res?;
3282 trace!(?event, "received network event");
3283 }
3284
3285 Ok(frontend_events)
3286 }
3287
3288 pub fn entity(&self) -> EcsEntity {
3289 self.state
3290 .ecs()
3291 .read_resource::<PlayerEntity>()
3292 .0
3293 .expect("Client::entity should always have PlayerEntity be Some")
3294 }
3295
3296 pub fn uid(&self) -> Option<Uid> { self.state.read_component_copied(self.entity()) }
3297
3298 pub fn presence(&self) -> Option<PresenceKind> { self.presence }
3299
3300 pub fn registered(&self) -> bool { self.registered }
3301
3302 pub fn get_tick(&self) -> u64 { self.tick }
3303
3304 pub fn get_ping_ms(&self) -> f64 { self.last_ping_delta * 1000.0 }
3305
3306 pub fn get_ping_ms_rolling_avg(&self) -> f64 {
3307 let mut total_weight = 0.;
3308 let pings = self.ping_deltas.len() as f64;
3309 (self
3310 .ping_deltas
3311 .iter()
3312 .enumerate()
3313 .fold(0., |acc, (i, ping)| {
3314 let weight = i as f64 + 1. / pings;
3315 total_weight += weight;
3316 acc + (weight * ping)
3317 })
3318 / total_weight)
3319 * 1000.0
3320 }
3321
3322 pub fn runtime(&self) -> &Arc<Runtime> { &self.runtime }
3327
3328 pub fn state(&self) -> &State { &self.state }
3330
3331 pub fn state_mut(&mut self) -> &mut State { &mut self.state }
3333
3334 pub fn players(&self) -> impl Iterator<Item = &str> {
3337 self.player_list()
3338 .values()
3339 .filter_map(|player_info| player_info.is_online.then_some(&*player_info.player_alias))
3340 }
3341
3342 pub fn is_moderator(&self) -> bool { self.role.is_some() }
3344
3345 pub fn role(&self) -> &Option<AdminRole> { &self.role }
3346
3347 fn clean_state(&mut self) {
3349 self.pending_trade = None;
3351
3352 let client_uid = self.uid().expect("Client doesn't have a Uid!!!");
3353
3354 self.state.ecs_mut().delete_all();
3356 self.state.ecs_mut().maintain();
3357 self.state.ecs_mut().insert(IdMaps::default());
3358
3359 let entity_builder = self.state.ecs_mut().create_entity();
3361 entity_builder
3362 .world
3363 .write_resource::<IdMaps>()
3364 .add_entity(client_uid, entity_builder.entity);
3365
3366 let entity = entity_builder.with(client_uid).build();
3367 self.state.ecs().write_resource::<PlayerEntity>().0 = Some(entity);
3368 }
3369
3370 #[deprecated = "this function doesn't localize"]
3375 fn personalize_alias(&self, uid: Uid, alias: String) -> String {
3376 let client_uid = self.uid().expect("Client doesn't have a Uid!!!");
3377 if client_uid == uid {
3378 "You".to_string()
3379 } else {
3380 alias
3381 }
3382 }
3383
3384 pub fn lookup_msg_context(&self, msg: &comp::ChatMsg) -> ChatTypeContext {
3387 let mut result = ChatTypeContext {
3388 you: self.uid().expect("Client doesn't have a Uid!!!"),
3389 player_info: HashMap::new(),
3390 entity_name: HashMap::new(),
3391 };
3392
3393 let name_of_uid = |uid| {
3394 let ecs = self.state().ecs();
3395 let id_maps = ecs.read_resource::<common::uid::IdMaps>();
3396 id_maps.uid_entity(uid).and_then(|e| {
3397 ecs.read_storage::<comp::Stats>()
3398 .get(e)
3399 .map(|s| s.name.clone())
3400 })
3401 };
3402
3403 let mut add_data_of = |uid| {
3404 match self.player_list.get(uid) {
3405 Some(player_info) => {
3406 result.player_info.insert(*uid, player_info.clone());
3407 },
3408 None => {
3409 result.entity_name.insert(
3410 *uid,
3411 name_of_uid(*uid).unwrap_or_else(|| Content::Plain("<?>".to_string())),
3412 );
3413 },
3414 };
3415 };
3416
3417 match &msg.chat_type {
3418 comp::ChatType::Online(uid) | comp::ChatType::Offline(uid) => add_data_of(uid),
3419 comp::ChatType::Kill(kill_source, victim) => {
3420 add_data_of(victim);
3421
3422 match kill_source {
3423 KillSource::Player(attacker_uid, _) => {
3424 add_data_of(attacker_uid);
3425 },
3426 KillSource::NonPlayer(_, _)
3427 | KillSource::FallDamage
3428 | KillSource::Suicide
3429 | KillSource::NonExistent(_)
3430 | KillSource::Other => (),
3431 };
3432 },
3433 comp::ChatType::Tell(from, to) | comp::ChatType::NpcTell(from, to) => {
3434 add_data_of(from);
3435 add_data_of(to);
3436 },
3437 comp::ChatType::Say(uid)
3438 | comp::ChatType::Region(uid)
3439 | comp::ChatType::World(uid)
3440 | comp::ChatType::NpcSay(uid)
3441 | comp::ChatType::Group(uid, _)
3442 | comp::ChatType::Faction(uid, _)
3443 | comp::ChatType::Npc(uid) => add_data_of(uid),
3444 comp::ChatType::CommandError
3445 | comp::ChatType::CommandInfo
3446 | comp::ChatType::FactionMeta(_)
3447 | comp::ChatType::GroupMeta(_)
3448 | comp::ChatType::Meta => (),
3449 };
3450 result
3451 }
3452
3453 #[cfg(feature = "tick_network")]
3462 #[expect(clippy::needless_collect)] pub fn tick_network(&mut self, dt: Duration) -> Result<(), Error> {
3464 span!(_guard, "tick_network", "Client::tick_network");
3465 self.state
3467 .ecs()
3468 .write_resource::<common::resources::ProgramTime>()
3469 .0 += dt.as_secs_f64();
3470
3471 let time_scale = *self
3472 .state
3473 .ecs()
3474 .read_resource::<common::resources::TimeScale>();
3475 self.state
3476 .ecs()
3477 .write_resource::<common::resources::Time>()
3478 .0 += dt.as_secs_f64() * time_scale.0;
3479
3480 self.handle_new_messages()?;
3482
3483 self.tick_terrain()?;
3485 let empty = Arc::new(TerrainChunk::new(
3486 0,
3487 Block::empty(),
3488 Block::empty(),
3489 common::terrain::TerrainChunkMeta::void(),
3490 ));
3491 let mut terrain = self.state.terrain_mut();
3492 let to_clear = terrain
3494 .iter()
3495 .filter_map(|(key, chunk)| (chunk.sub_chunks_len() != 0).then(|| key))
3496 .collect::<Vec<_>>();
3497 to_clear.into_iter().for_each(|key| {
3498 terrain.insert(key, Arc::clone(&empty));
3499 });
3500 drop(terrain);
3501
3502 if self.state.get_program_time() - self.last_server_ping > 1. {
3504 self.send_msg_err(PingMsg::Ping)?;
3505 self.last_server_ping = self.state.get_program_time();
3506 }
3507
3508 if self.presence.is_some() {
3510 if let (Some(pos), Some(vel), Some(ori)) = (
3511 self.state.read_storage().get(self.entity()).cloned(),
3512 self.state.read_storage().get(self.entity()).cloned(),
3513 self.state.read_storage().get(self.entity()).cloned(),
3514 ) {
3515 self.in_game_stream.send(ClientGeneral::PlayerPhysics {
3516 pos,
3517 vel,
3518 ori,
3519 force_counter: self.force_update_counter,
3520 })?;
3521 }
3522 }
3523
3524 self.tick += 1;
3526
3527 Ok(())
3528 }
3529
3530 pub fn plugin_received(&mut self, hash: PluginHash) -> usize {
3532 if !self.missing_plugins.remove(&hash) {
3533 tracing::warn!(?hash, "received unrequested plugin");
3534 }
3535 self.missing_plugins.len()
3536 }
3537
3538 pub fn are_plugins_missing(&self) -> bool { !self.missing_plugins.is_empty() }
3540
3541 pub fn take_local_plugins(&mut self) -> Vec<PathBuf> { std::mem::take(&mut self.local_plugins) }
3543}
3544
3545impl Drop for Client {
3546 fn drop(&mut self) {
3547 trace!("Dropping client");
3548 if self.registered {
3549 if let Err(e) = self.send_msg_err(ClientGeneral::Terminate) {
3550 warn!(
3551 ?e,
3552 "Error during drop of client, couldn't send disconnect package, is the \
3553 connection already closed?",
3554 );
3555 }
3556 } else {
3557 trace!("no disconnect msg necessary as client wasn't registered")
3558 }
3559
3560 tokio::task::block_in_place(|| {
3561 if let Err(e) = self
3562 .runtime
3563 .block_on(self.participant.take().unwrap().disconnect())
3564 {
3565 warn!(?e, "error when disconnecting, couldn't send all data");
3566 }
3567 });
3568 drop(self.network.take());
3570 }
3571}
3572
3573#[cfg(test)]
3574mod tests {
3575 use super::*;
3576 use client_i18n::LocalizationHandle;
3577
3578 #[test]
3579 fn constant_api_test() {
3586 use common::clock::Clock;
3587 use voxygen_i18n_helpers::localize_chat_message;
3588
3589 const SPT: f64 = 1.0 / 60.0;
3590
3591 let runtime = Arc::new(Runtime::new().unwrap());
3592 let runtime2 = Arc::clone(&runtime);
3593 let username = "Foo";
3594 let password = "Bar";
3595 let auth_server = "auth.veloren.net";
3596 let veloren_client: Result<Client, Error> = runtime.block_on(Client::new(
3597 ConnectionArgs::Tcp {
3598 hostname: "127.0.0.1:9000".to_owned(),
3599 prefer_ipv6: false,
3600 },
3601 runtime2,
3602 &mut None,
3603 username,
3604 password,
3605 None,
3606 |suggestion: &str| suggestion == auth_server,
3607 &|_| {},
3608 |_| {},
3609 PathBuf::default(),
3610 ClientType::ChatOnly,
3611 ));
3612 let localisation = LocalizationHandle::load_expect("en");
3613
3614 let _ = veloren_client.map(|mut client| {
3615 let mut clock = Clock::new(Duration::from_secs_f64(SPT));
3617
3618 let events_result: Result<Vec<Event>, Error> =
3620 client.tick(ControllerInputs::default(), clock.game_dt());
3621
3622 client.send_chat("foobar".to_string());
3624
3625 let _ = events_result.map(|mut events| {
3626 if let Some(event) = events.pop() {
3628 match event {
3629 Event::Chat(msg) => {
3630 let msg: comp::ChatMsg = msg;
3631 let _s: String = localize_chat_message(
3632 &msg,
3633 &client.lookup_msg_context(&msg),
3634 &localisation.read(),
3635 true,
3636 )
3637 .1;
3638 },
3639 Event::Disconnect => {},
3640 Event::DisconnectionNotification(_) => {
3641 debug!("Will be disconnected soon! :/")
3642 },
3643 Event::Notification(notification) => {
3644 let notification: UserNotification = notification;
3645 debug!("Notification: {:?}", notification);
3646 },
3647 _ => {},
3648 }
3649 };
3650 });
3651
3652 client.cleanup();
3653 clock.tick();
3654 });
3655 }
3656}