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