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