veloren_client/
lib.rs

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