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