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