Skip to main content

veloren_client/
lib.rs

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