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