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