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