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