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