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