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