1#![deny(unsafe_code)]
2#![deny(clippy::clone_on_ref_ptr)]
3
4pub mod addr;
5pub mod error;
6mod init;
7
8pub 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#[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#[derive(Debug)]
144pub enum UserNotification {
145 WaypointUpdated,
146}
147
148pub struct WorldData {
149 pub lod_base: Grid<u32>,
153 pub lod_alt: Grid<u32>,
157 pub lod_horizon: Grid<u32>,
161 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 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 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 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 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 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 invite: Option<(Uid, Instant, Duration, InviteKind)>,
306 group_leader: Option<Uid>,
307 group_members: HashMap<Uid, group::Role>,
309 pending_invites: HashSet<Uid>,
311 pending_trade: Option<(TradeId, PendingTrade, Option<SitePrices>)>,
313 waypoint: Option<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 server_view_distance_limit: Option<u32>,
337 view_distance: Option<u32>,
338 lod_distance: f32,
339 loaded_distance: f32,
341
342 pending_chunks: HashMap<Vec2<i32>, Instant>,
343 target_time_of_day: Option<TimeOfDay>,
344 dt_adjustment: f64,
345
346 connected_server_constants: ServerConstants,
347 missing_plugins: HashSet<PluginHash>,
349 local_plugins: Vec<PathBuf>,
351}
352
353#[derive(Debug, Default)]
356pub struct CharacterList {
357 pub characters: Vec<CharacterItem<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(); 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 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 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 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 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 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 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 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 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 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 let pools = State::pools(GameMode::Client);
612 let mut state = State::client(
613 pools,
614 map_size_lg,
615 world_map.default_chunk,
616 |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 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!("Server requires plugin {hash:x?}");
641 missing_plugins.push(*hash);
642 }
643 }
644 }
645 }
646 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 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 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 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 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 Rgb::new(0.15, 0.15, 0.15)
788 } else {
789 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 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 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 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 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 .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 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 if !credential_provider.auth_trusted(&server_info.auth_provider) {
1049 return Err(Error::AuthServerNotTrusted);
1050 }
1051
1052 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 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 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 ClientGeneral::TerrainChunkRequest { .. }
1162 | ClientGeneral::LodZoneRequest { .. } => {
1163 #[cfg(feature = "tracy")]
1164 {
1165 terrain = 1.0;
1166 }
1167 &mut self.terrain_stream
1168 },
1169 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 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 self.presence = Some(PresenceKind::Character(character_id));
1231 }
1232
1233 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 pub fn load_character_list(&mut self) {
1243 self.character_list.loading = true;
1244 self.send_msg(ClientGeneral::RequestCharacterList);
1245 }
1246
1247 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 pub fn delete_character(&mut self, character_id: CharacterId) {
1275 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 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 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 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 if let Some(uid) = self.state.read_component_copied(entity) {
1429 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 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 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 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 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 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 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 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 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 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)) }
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 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 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 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 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 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 pub fn swap_loadout(&mut self) { self.control_action(ControlAction::SwapEquippedWeapons) }
1886
1887 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 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 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 pub fn send_chat(&mut self, message: String) {
2147 self.send_msg(ClientGeneral::ChatMsg(comp::Content::Plain(message)));
2148 }
2149
2150 pub fn send_command(&mut self, name: String, args: Vec<String>) {
2152 self.send_msg(ClientGeneral::Command(name, args));
2153 }
2154
2155 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 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 pub fn tick(&mut self, inputs: ControllerInputs, dt: Duration) -> Result<Vec<Event>, Error> {
2254 span!(_guard, "tick", "Client::tick");
2255 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 let mut frontend_events = Vec::new();
2304
2305 {
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 .filter(|l| !character_state.same_variant(&l.0))
2324 {
2325 *l = comp::Last(character_state.clone());
2326 }
2327 }
2328 }
2329
2330 frontend_events.append(&mut self.handle_new_messages()?);
2332
2333 if self
2336 .invite
2337 .is_some_and(|(_, timeout, dur, _)| timeout.elapsed() > dur)
2338 {
2339 self.invite = None;
2340 }
2341
2342 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 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 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 let _ = self.state.ecs().fetch::<EventBus<Outcome>>().recv_all();
2376
2377 self.tick_terrain()?;
2379
2380 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 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 self.tick += 1;
2416 Ok(frontend_events)
2417 }
2418
2419 pub fn cleanup(&mut self) {
2421 self.state.cleanup();
2423 }
2424
2425 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 let mut chunks_to_remove = Vec::new();
2441 self.state.terrain().iter().for_each(|(key, _)| {
2442 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 self.loaded_distance = ((view_distance * TerrainChunkSize::RECT_SIZE.x) as f32).powi(2);
2464 for dist in 0..view_distance as i32 + 1 {
2466 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 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 let lod_zone = lod_pos.map(|e| lod::from_wpos(e as i32));
2544
2545 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 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 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 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 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 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 match change_notification {
2804 Added(uid, role) => {
2805 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 .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 #[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 #[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 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 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 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 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 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 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 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 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 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 pub fn runtime(&self) -> &Arc<Runtime> { &self.runtime }
3242
3243 pub fn state(&self) -> &State { &self.state }
3245
3246 pub fn state_mut(&mut self) -> &mut State { &mut self.state }
3248
3249 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 pub fn is_moderator(&self) -> bool { self.role.is_some() }
3259
3260 pub fn role(&self) -> &Option<AdminRole> { &self.role }
3261
3262 fn clean_state(&mut self) {
3264 self.pending_trade = None;
3266
3267 let client_uid = self.uid().expect("Client doesn't have a Uid!!!");
3268
3269 self.state.ecs_mut().delete_all();
3271 self.state.ecs_mut().maintain();
3272 self.state.ecs_mut().insert(IdMaps::default());
3273
3274 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 #[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 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 #[cfg(feature = "tick_network")]
3377 #[expect(clippy::needless_collect)] pub fn tick_network(&mut self, dt: Duration) -> Result<(), Error> {
3379 span!(_guard, "tick_network", "Client::tick_network");
3380 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 self.handle_new_messages()?;
3397
3398 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 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 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 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 self.tick += 1;
3441
3442 Ok(())
3443 }
3444
3445 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 pub fn are_plugins_missing(&self) -> bool { !self.missing_plugins.is_empty() }
3455
3456 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 drop(self.network.take());
3485 }
3486}
3487
3488#[cfg(test)]
3489mod tests {
3490 use super::*;
3491 use client_i18n::LocalizationHandle;
3492
3493 #[test]
3494 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 let mut clock = Clock::new(Duration::from_secs_f64(SPT));
3538
3539 let events_result: Result<Vec<Event>, Error> =
3541 client.tick(ControllerInputs::default(), clock.game_dt());
3542
3543 client.send_chat("foobar".to_string());
3545
3546 let _ = events_result.map(|mut events| {
3547 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}