veloren_server/
state_ext.rs

1#[cfg(feature = "worldgen")]
2use crate::rtsim::RtSim;
3use crate::{
4    BattleModeBuffer, SpawnPoint,
5    automod::AutoMod,
6    chat::ChatExporter,
7    client::Client,
8    events::{self, shared::update_map_markers},
9    persistence::PersistedComponents,
10    pet::restore_pet,
11    presence::RepositionToFreeSpace,
12    settings::Settings,
13    sys::sentinel::DeletedEntities,
14    wiring,
15};
16use common::{
17    LoadoutBuilder, ViewDistances,
18    character::CharacterId,
19    comp::{
20        self, BASE_ABILITY_LIMIT, CapsulePrism, ChatType, Content, Group, Inventory, LootOwner,
21        Object, Player, Poise, Presence, PresenceKind, item::ItemKind, misc::PortalData, object,
22    },
23    interaction::Interaction,
24    link::{Is, Link, LinkHandle},
25    mounting::{Mounting, Rider, VolumeMounting, VolumeRider},
26    resources::{Secs, Time},
27    rtsim::{Actor, RtSimEntity},
28    tether::Tethered,
29    uid::{IdMaps, Uid},
30    util::Dir,
31};
32#[cfg(feature = "worldgen")]
33use common::{calendar::Calendar, resources::TimeOfDay, slowjob::SlowJobPool};
34use common_net::{
35    msg::{CharacterInfo, PlayerListUpdate, ServerGeneral},
36    sync::WorldSyncExt,
37};
38use common_state::State;
39use specs::{
40    Builder, Entity as EcsEntity, EntityBuilder as EcsEntityBuilder, Join, WorldExt, WriteStorage,
41    storage::{GenericReadStorage, GenericWriteStorage},
42};
43use std::time::{Duration, Instant};
44use tracing::{error, trace, warn};
45use vek::*;
46
47pub trait StateExt {
48    /// Build a non-player character
49    fn create_npc(
50        &mut self,
51        pos: comp::Pos,
52        ori: comp::Ori,
53        stats: comp::Stats,
54        skill_set: comp::SkillSet,
55        health: Option<comp::Health>,
56        poise: Poise,
57        inventory: Inventory,
58        body: comp::Body,
59        scale: comp::Scale,
60    ) -> EcsEntityBuilder<'_>;
61    /// Create an entity with only a position
62    fn create_empty(&mut self, pos: comp::Pos) -> EcsEntityBuilder<'_>;
63    /// Build a static object entity
64    fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body)
65    -> EcsEntityBuilder<'_>;
66    /// Create an item drop or merge the item with an existing drop, if a
67    /// suitable candidate exists.
68    fn create_item_drop(
69        &mut self,
70        pos: comp::Pos,
71        ori: comp::Ori,
72        vel: comp::Vel,
73        item: comp::PickupItem,
74        loot_owner: Option<LootOwner>,
75    ) -> Option<EcsEntity>;
76    fn create_ship<F: FnOnce(comp::ship::Body) -> comp::Collider>(
77        &mut self,
78        pos: comp::Pos,
79        ori: comp::Ori,
80        ship: comp::ship::Body,
81        make_collider: F,
82    ) -> EcsEntityBuilder<'_>;
83    /// Build a projectile
84    fn create_projectile(
85        &mut self,
86        pos: comp::Pos,
87        vel: comp::Vel,
88        body: comp::Body,
89        projectile: comp::Projectile,
90    ) -> EcsEntityBuilder<'_>;
91    /// Build a shockwave entity
92    fn create_shockwave(
93        &mut self,
94        properties: comp::shockwave::Properties,
95        pos: comp::Pos,
96        ori: comp::Ori,
97    ) -> EcsEntityBuilder<'_>;
98    fn create_arcing(
99        &mut self,
100        arc: comp::ArcProperties,
101        target: Uid,
102        owner: Option<Uid>,
103        pos: comp::Pos,
104    ) -> EcsEntityBuilder<'_>;
105    /// Creates a safezone
106    fn create_safezone(&mut self, range: Option<f32>, pos: comp::Pos) -> EcsEntityBuilder<'_>;
107    fn create_wiring(
108        &mut self,
109        pos: comp::Pos,
110        object: comp::object::Body,
111        wiring_element: wiring::WiringElement,
112    ) -> EcsEntityBuilder<'_>;
113    // NOTE: currently only used for testing
114    /// Queues chunk generation in the view distance of the persister, this
115    /// entity must be built before those chunks are received (the builder
116    /// borrows the ecs world so that is kind of impossible in practice)
117    #[cfg(feature = "worldgen")]
118    fn create_persister(
119        &mut self,
120        pos: comp::Pos,
121        view_distance: u32,
122        world: &std::sync::Arc<world::World>,
123        index: &world::IndexOwned,
124    ) -> EcsEntityBuilder<'_>;
125    /// Creates a teleporter entity, which allows players to teleport to the
126    /// `target` position. You might want to require the teleporting entity
127    /// to not have agro for teleporting.
128    fn create_teleporter(&mut self, pos: comp::Pos, portal: PortalData) -> EcsEntityBuilder<'_>;
129    /// Insert common/default components for a new character joining the server
130    fn initialize_character_data(
131        &mut self,
132        entity: EcsEntity,
133        character_id: CharacterId,
134        view_distances: ViewDistances,
135    );
136    /// Insert common/default components for a new spectator joining the server
137    fn initialize_spectator_data(&mut self, entity: EcsEntity, view_distances: ViewDistances);
138    /// Update the components associated with the entity's current character.
139    /// Performed after loading component data from the database
140    fn update_character_data(
141        &mut self,
142        entity: EcsEntity,
143        components: PersistedComponents,
144    ) -> Result<(), String>;
145    /// Iterates over registered clients and send each `ServerMsg`
146    fn validate_chat_msg(
147        &self,
148        player: EcsEntity,
149        chat_type: &comp::ChatType<comp::Group>,
150        msg: &Content,
151        // Whether the message is directly from a client or was generated on the server.
152        //
153        // Note, clients can influence the content of messages generated on the server (e.g. via
154        // chat commands like `/tell`), this is just used for logging purposes.
155        from_client: bool,
156    ) -> bool;
157    fn send_chat(&self, msg: comp::UnresolvedChatMsg, from_client: bool);
158    fn notify_players(&self, msg: ServerGeneral);
159    fn notify_in_game_clients(&self, msg: ServerGeneral);
160    /// Create a new link between entities (see [`common::mounting`] for an
161    /// example).
162    fn link<L: Link>(&mut self, link: L) -> Result<(), L::Error>;
163    /// Maintain active links between entities
164    fn maintain_links(&mut self);
165    /// Delete an entity, recording the deletion in [`DeletedEntities`]
166    fn delete_entity_recorded(
167        &mut self,
168        entity: EcsEntity,
169    ) -> Result<(), specs::error::WrongGeneration>;
170    /// Get the given entity as an [`Actor`], if it is one.
171    fn entity_as_actor(&self, entity: EcsEntity) -> Option<Actor>;
172    /// Mutate the position of an entity or, if the entity is mounted, the
173    /// mount.
174    ///
175    /// If `dismount_volume` is `true`, an entity mounted on a volume entity
176    /// (such as an airship) will be dismounted to avoid teleporting the volume
177    /// entity.
178    fn position_mut<T>(
179        &mut self,
180        entity: EcsEntity,
181        dismount_volume: bool,
182        f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T,
183    ) -> Result<T, Content>;
184
185    /// Mutate the position of an entity or, if the entity is mounted, the
186    /// mount.
187    ///
188    /// If `needs_ground` is set to false, the entity will strictly be mutated
189    /// to the nearest available space on the ground
190    /// otherwise, the entity will be mutated to the nearest available space
191    /// regardless of it being the ground or the sky.
192    ///
193    /// If `dismount_volume` is `true`, an entity mounted on a volume entity
194    /// (such as an airship) will be dismounted to avoid teleporting the volume
195    /// entity.
196    fn position_mut_reposition<T>(
197        &mut self,
198        entity: EcsEntity,
199        dismount_volume: bool,
200        f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T,
201        needs_ground: bool,
202        modify_waypoints: bool,
203    ) -> Result<T, Content>;
204}
205
206impl StateExt for State {
207    fn create_npc(
208        &mut self,
209        pos: comp::Pos,
210        ori: comp::Ori,
211        stats: comp::Stats,
212        skill_set: comp::SkillSet,
213        health: Option<comp::Health>,
214        poise: Poise,
215        inventory: Inventory,
216        body: comp::Body,
217        scale: comp::Scale,
218    ) -> EcsEntityBuilder<'_> {
219        self.ecs_mut()
220            .create_entity_synced()
221            .with(pos)
222            .with(comp::Vel(Vec3::zero()))
223            .with(ori)
224            .with(comp::Mass(body.mass().0 * scale.0.powi(3)))
225            .with(body.density())
226            .with(body.collider())
227            .with(scale)
228            .with(comp::Controller::default())
229            .with(body)
230            .with(comp::Energy::new(body))
231            .with(stats)
232            .with(if body.is_humanoid() {
233                comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT)
234            } else {
235                comp::ActiveAbilities::default()
236            })
237            .with(skill_set)
238            .maybe_with(health)
239            .with(poise)
240            .with(comp::Alignment::Npc)
241            .with(comp::CharacterState::default())
242            .with(comp::CharacterActivity::default())
243            .with(inventory)
244            .with(comp::Buffs::default())
245            .with(comp::Combo::default())
246            .with(comp::Auras::default())
247            .with(comp::EnteredAuras::default())
248            .with(comp::Stance::default())
249            .with(comp::projectile::ProjectileHitEntities::default())
250            .maybe_with(body.heads().map(comp::body::parts::Heads::new))
251    }
252
253    fn create_empty(&mut self, pos: comp::Pos) -> EcsEntityBuilder<'_> {
254        self.ecs_mut()
255            .create_entity_synced()
256            .with(pos)
257            .with(comp::Vel(Vec3::zero()))
258            .with(comp::Ori::default())
259    }
260
261    fn create_object(
262        &mut self,
263        pos: comp::Pos,
264        object: comp::object::Body,
265    ) -> EcsEntityBuilder<'_> {
266        let body = comp::Body::Object(object);
267        self.create_empty(pos)
268            .with(body.mass())
269            .with(body.density())
270            .with(body.collider())
271            .with(body)
272    }
273
274    fn create_item_drop(
275        &mut self,
276        pos: comp::Pos,
277        ori: comp::Ori,
278        vel: comp::Vel,
279        world_item: comp::PickupItem,
280        loot_owner: Option<LootOwner>,
281    ) -> Option<EcsEntity> {
282        // Attempt merging with any nearby entities if possible
283        {
284            use crate::sys::item::get_nearby_mergeable_items;
285
286            let positions = self.ecs().read_storage::<comp::Pos>();
287            let loot_owners = self.ecs().read_storage::<LootOwner>();
288            let mut items = self.ecs().write_storage::<comp::PickupItem>();
289            let entities = self.ecs().entities();
290            let spatial_grid = self.ecs().read_resource();
291
292            let nearby_items = get_nearby_mergeable_items(
293                &world_item,
294                &pos,
295                loot_owner.as_ref(),
296                (&entities, &items, &positions, &loot_owners, &spatial_grid),
297            );
298
299            // Merge the nearest item if possible, skip to creating a drop otherwise
300            if let Some((mergeable_item, _)) =
301                nearby_items.min_by_key(|(_, dist)| (dist * 1000.0) as i32)
302            {
303                items
304                    .get_mut(mergeable_item)
305                    .expect("we know that the item exists")
306                    .try_merge(world_item)
307                    .expect("`try_merge` should succeed because `can_merge` returned `true`");
308                return None;
309            }
310        }
311
312        let spawned_at = *self.ecs().read_resource::<Time>();
313
314        let item_body = comp::body::item::Body::from(world_item.item());
315        let body = comp::Body::Item(item_body);
316        let light_emitter = match &*world_item.item().kind() {
317            ItemKind::Lantern(lantern) => Some(comp::LightEmitter {
318                col: lantern.color(),
319                strength: lantern.strength(),
320                flicker: lantern.flicker(),
321                animated: true,
322                dir: lantern.dir,
323            }),
324            _ => None,
325        };
326        Some(
327            self.ecs_mut()
328                .create_entity_synced()
329                .with(world_item)
330                .with(pos)
331                .with(ori)
332                .with(vel)
333                .with(item_body.orientation(&mut rand::rng()))
334                .with(item_body.mass())
335                .with(item_body.density())
336                .with(body.collider())
337                .with(body)
338                .with(Object::DeleteAfter {
339                    spawned_at,
340                    // Delete the item drop after 5 minutes
341                    timeout: Duration::from_secs(300),
342                })
343                .maybe_with(loot_owner)
344                .maybe_with(light_emitter)
345                .build(),
346        )
347    }
348
349    fn create_ship<F: FnOnce(comp::ship::Body) -> comp::Collider>(
350        &mut self,
351        pos: comp::Pos,
352        ori: comp::Ori,
353        ship: comp::ship::Body,
354        make_collider: F,
355    ) -> EcsEntityBuilder<'_> {
356        let body = comp::Body::Ship(ship);
357
358        self
359            .ecs_mut()
360            .create_entity_synced()
361            .with(pos)
362            .with(comp::Vel(Vec3::zero()))
363            .with(ori)
364            .with(body.mass())
365            .with(body.density())
366            .with(make_collider(ship))
367            .with(body)
368            .with(comp::Controller::default())
369            .with(Inventory::with_empty())
370            .with(comp::CharacterState::default())
371            .with(comp::CharacterActivity::default())
372            // TODO: some of these are required in order for the
373            // character_behavior system to recognize a possesed airship;
374            // that system should be refactored to use `.maybe()`
375            .with(comp::Energy::new(ship.into()))
376            .with(comp::Stats::new({
377                // TODO: I hope it is possible to localize it safely, but
378                // at the same time it's not visible anyway, so let's postpone
379                // this.
380                Content::Plain("Airship".to_string())
381            }, body))
382            .with(comp::SkillSet::default())
383            .with(comp::ActiveAbilities::default())
384            .with(comp::Combo::default())
385    }
386
387    fn create_projectile(
388        &mut self,
389        pos: comp::Pos,
390        vel: comp::Vel,
391        body: comp::Body,
392        projectile: comp::Projectile,
393    ) -> EcsEntityBuilder<'_> {
394        let mut projectile_base = self
395            .ecs_mut()
396            .create_entity_synced()
397            .with(pos)
398            .with(vel)
399            .with(comp::Ori::from_unnormalized_vec(vel.0).unwrap_or_default())
400            .with(body.mass())
401            .with(body.density());
402
403        if projectile.is_sticky {
404            projectile_base = projectile_base.with(comp::Sticky);
405        }
406        if projectile.is_point {
407            projectile_base = projectile_base.with(comp::Collider::Point);
408        } else {
409            projectile_base = projectile_base.with(body.collider());
410        }
411
412        projectile_base.with(projectile).with(body)
413    }
414
415    fn create_shockwave(
416        &mut self,
417        properties: comp::shockwave::Properties,
418        pos: comp::Pos,
419        ori: comp::Ori,
420    ) -> EcsEntityBuilder<'_> {
421        self.ecs_mut()
422            .create_entity_synced()
423            .with(pos)
424            .with(ori)
425            .with(comp::Shockwave {
426                properties,
427                creation: None,
428            })
429            .with(comp::ShockwaveHitEntities {
430                hit_entities: Vec::<Uid>::new(),
431            })
432    }
433
434    fn create_arcing(
435        &mut self,
436        arc: comp::ArcProperties,
437        target: Uid,
438        owner: Option<Uid>,
439        pos: comp::Pos,
440    ) -> EcsEntityBuilder<'_> {
441        let time = self.get_time();
442
443        self.ecs_mut()
444            .create_entity_synced()
445            .with(pos)
446            .with(comp::Arcing {
447                properties: arc,
448                last_arc_time: Time(time),
449                hit_entities: vec![target],
450                owner,
451            })
452    }
453
454    fn create_safezone(&mut self, range: Option<f32>, pos: comp::Pos) -> EcsEntityBuilder<'_> {
455        use comp::{
456            aura::{Aura, AuraKind, AuraTarget, Auras},
457            buff::{BuffCategory, BuffData, BuffKind, BuffSource},
458        };
459        let time = self.get_time();
460        // TODO: Consider using the area system for this
461        self.ecs_mut()
462            .create_entity_synced()
463            .with(pos)
464            .with(Auras::new(vec![Aura::new(
465                AuraKind::Buff {
466                    kind: BuffKind::Invulnerability,
467                    data: BuffData::new(1.0, Some(Secs(1.0))),
468                    category: BuffCategory::Natural,
469                    source: BuffSource::World,
470                },
471                range.unwrap_or(100.0),
472                None,
473                AuraTarget::All,
474                Time(time),
475            )]))
476    }
477
478    fn create_wiring(
479        &mut self,
480        pos: comp::Pos,
481        object: comp::object::Body,
482        wiring_element: wiring::WiringElement,
483    ) -> EcsEntityBuilder<'_> {
484        self.ecs_mut()
485            .create_entity_synced()
486            .with(pos)
487            .with(comp::Vel(Vec3::zero()))
488            .with(comp::Ori::default())
489            .with({
490                let body: comp::Body = object.into();
491                body.collider()
492            })
493            .with(comp::Body::Object(object))
494            .with(comp::Mass(100.0))
495            // .with(comp::Sticky)
496            .with(wiring_element)
497            .with(comp::LightEmitter {
498                col: Rgb::new(0.0, 0.0, 0.0),
499                strength: 2.0,
500                flicker: 1.0,
501                animated: true,
502                dir: None,
503            })
504    }
505
506    // NOTE: currently only used for testing
507    /// Queues chunk generation in the view distance of the persister, this
508    /// entity must be built before those chunks are received (the builder
509    /// borrows the ecs world so that is kind of impossible in practice)
510    #[cfg(feature = "worldgen")]
511    fn create_persister(
512        &mut self,
513        pos: comp::Pos,
514        view_distance: u32,
515        world: &std::sync::Arc<world::World>,
516        index: &world::IndexOwned,
517    ) -> EcsEntityBuilder<'_> {
518        use common::{terrain::TerrainChunkSize, vol::RectVolSize};
519        use std::sync::Arc;
520        // Request chunks
521        {
522            let ecs = self.ecs();
523            let slow_jobs = ecs.write_resource::<SlowJobPool>();
524            let rtsim = ecs.read_resource::<RtSim>();
525            let mut chunk_generator =
526                ecs.write_resource::<crate::chunk_generator::ChunkGenerator>();
527            let chunk_pos = self.terrain().pos_key(pos.0.map(|e| e as i32));
528            (-(view_distance as i32)..view_distance as i32 + 1)
529            .flat_map(|x| {
530                (-(view_distance as i32)..view_distance as i32 + 1).map(move |y| Vec2::new(x, y))
531            })
532            .map(|offset| offset + chunk_pos)
533            // Filter chunks outside the view distance
534            // Note: calculation from client chunk request filtering
535            .filter(|chunk_key| {
536                pos.0.xy().map(|e| e as f64).distance(
537                    chunk_key.map(|e| e as f64 + 0.5) * TerrainChunkSize::RECT_SIZE.map(|e| e as f64),
538                ) < (view_distance as f64 - 1.0 + 2.5 * 2.0_f64.sqrt())
539                    * TerrainChunkSize::RECT_SIZE.x as f64
540            })
541            .for_each(|chunk_key| {
542                {
543                    let time = (*ecs.read_resource::<TimeOfDay>(), (*ecs.read_resource::<Calendar>()).clone());
544                    chunk_generator.generate_chunk(None, chunk_key, &slow_jobs, Arc::clone(world), &rtsim, index.clone(), time);
545                }
546            });
547        }
548
549        self.ecs_mut()
550            .create_entity_synced()
551            .with(pos)
552            .with(Presence::new(
553                ViewDistances {
554                    terrain: view_distance,
555                    entity: view_distance,
556                },
557                PresenceKind::Spectator,
558            ))
559    }
560
561    fn create_teleporter(&mut self, pos: comp::Pos, portal: PortalData) -> EcsEntityBuilder<'_> {
562        self.create_object(pos, object::Body::Portal)
563            .with(comp::Immovable)
564            .with(comp::Object::from(portal))
565    }
566
567    fn initialize_character_data(
568        &mut self,
569        entity: EcsEntity,
570        character_id: CharacterId,
571        view_distances: ViewDistances,
572    ) {
573        let spawn_point = self.ecs().read_resource::<SpawnPoint>().0;
574
575        if let Some(player_uid) = self.read_component_copied::<Uid>(entity) {
576            // NOTE: By fetching the player_uid, we validated that the entity exists, and we
577            // call nothing that can delete it in any of the subsequent
578            // commands, so we can assume that all of these calls succeed,
579            // justifying ignoring the result of insertion.
580            self.write_component_ignore_entity_dead(entity, comp::Controller::default());
581            self.write_component_ignore_entity_dead(entity, comp::Pos(spawn_point));
582            self.write_component_ignore_entity_dead(entity, comp::Vel(Vec3::zero()));
583            self.write_component_ignore_entity_dead(entity, comp::Ori::default());
584            self.write_component_ignore_entity_dead(
585                entity,
586                comp::Collider::CapsulePrism(CapsulePrism {
587                    p0: Vec2::zero(),
588                    p1: Vec2::zero(),
589                    radius: 0.4,
590                    z_min: 0.0,
591                    z_max: 1.75,
592                }),
593            );
594            self.write_component_ignore_entity_dead(entity, comp::CharacterState::default());
595            self.write_component_ignore_entity_dead(entity, comp::CharacterActivity::default());
596            self.write_component_ignore_entity_dead(entity, comp::Alignment::Owned(player_uid));
597            self.write_component_ignore_entity_dead(entity, comp::Buffs::default());
598            self.write_component_ignore_entity_dead(entity, comp::Auras::default());
599            self.write_component_ignore_entity_dead(entity, comp::EnteredAuras::default());
600            self.write_component_ignore_entity_dead(entity, comp::Combo::default());
601            self.write_component_ignore_entity_dead(entity, comp::Stance::default());
602            self.write_component_ignore_entity_dead(
603                entity,
604                comp::projectile::ProjectileHitEntities::default(),
605            );
606
607            // Make sure physics components are updated
608            self.write_component_ignore_entity_dead(entity, comp::ForceUpdate::forced());
609
610            self.write_component_ignore_entity_dead(
611                entity,
612                Presence::new(view_distances, PresenceKind::LoadingCharacter(character_id)),
613            );
614
615            // Tell the client its request was successful.
616            if let Some(client) = self.ecs().read_storage::<Client>().get(entity) {
617                client.send_fallible(ServerGeneral::CharacterSuccess);
618            }
619        }
620    }
621
622    fn initialize_spectator_data(&mut self, entity: EcsEntity, view_distances: ViewDistances) {
623        let spawn_point = self.ecs().read_resource::<SpawnPoint>().0;
624
625        if self.read_component_copied::<Uid>(entity).is_some() {
626            // NOTE: By fetching the player_uid, we validated that the entity exists, and we
627            // call nothing that can delete it in any of the subsequent
628            // commands, so we can assume that all of these calls succeed,
629            // justifying ignoring the result of insertion.
630            self.write_component_ignore_entity_dead(entity, comp::Pos(spawn_point));
631
632            // Make sure physics components are updated
633            self.write_component_ignore_entity_dead(entity, comp::ForceUpdate::forced());
634
635            self.write_component_ignore_entity_dead(
636                entity,
637                Presence::new(view_distances, PresenceKind::Spectator),
638            );
639
640            // Tell the client its request was successful.
641            if let Some(client) = self.ecs().read_storage::<Client>().get(entity) {
642                client.send_fallible(ServerGeneral::SpectatorSuccess(spawn_point));
643            }
644        }
645    }
646
647    /// Returned error intended to be sent to the client.
648    fn update_character_data(
649        &mut self,
650        entity: EcsEntity,
651        components: PersistedComponents,
652    ) -> Result<(), String> {
653        let PersistedComponents {
654            body,
655            hardcore,
656            stats,
657            skill_set,
658            inventory,
659            waypoint,
660            pets,
661            active_abilities,
662            map_marker,
663        } = components;
664
665        if let Some(player_uid) = self.read_component_copied::<Uid>(entity) {
666            let result =
667                if let Some(presence) = self.ecs().write_storage::<Presence>().get_mut(entity) {
668                    if let PresenceKind::LoadingCharacter(id) = presence.kind {
669                        presence.kind = PresenceKind::Character(id);
670                        self.ecs()
671                            .write_resource::<IdMaps>()
672                            .add_character(id, entity);
673                        Ok(())
674                    } else {
675                        Err("PresenceKind is not LoadingCharacter")
676                    }
677                } else {
678                    Err("Presence component missing")
679                };
680            if let Err(err) = result {
681                let err = format!("Unexpected state when applying loaded character info: {err}");
682                error!("{err}");
683                // TODO: we could produce a `comp::Content` for this to allow localization.
684                return Err(err);
685            }
686
687            let name = stats.name.clone();
688            // NOTE: hack, read docs on body::Gender for more
689            let gender = stats.original_body.humanoid_gender();
690
691            // NOTE: By fetching the player_uid, we validated that the entity exists,
692            // and we call nothing that can delete it in any of the subsequent
693            // commands, so we can assume that all of these calls succeed,
694            // justifying ignoring the result of insertion.
695            self.write_component_ignore_entity_dead(entity, body.collider());
696            self.write_component_ignore_entity_dead(entity, body);
697            self.write_component_ignore_entity_dead(entity, body.mass());
698            self.write_component_ignore_entity_dead(entity, body.density());
699            self.write_component_ignore_entity_dead(entity, comp::Health::new(body));
700            self.write_component_ignore_entity_dead(entity, comp::Energy::new(body));
701            self.write_component_ignore_entity_dead(entity, Poise::new(body));
702            self.write_component_ignore_entity_dead(entity, stats);
703            self.write_component_ignore_entity_dead(entity, active_abilities);
704            self.write_component_ignore_entity_dead(entity, skill_set);
705            self.write_component_ignore_entity_dead(entity, inventory);
706            self.write_component_ignore_entity_dead(
707                entity,
708                comp::InventoryUpdateBuffer::new(comp::InventoryUpdateEvent::Init),
709            );
710
711            if let Some(hardcore) = hardcore {
712                self.write_component_ignore_entity_dead(entity, hardcore);
713            }
714
715            if let Some(waypoint) = waypoint {
716                self.write_component_ignore_entity_dead(entity, RepositionToFreeSpace {
717                    needs_ground: true,
718                    modify_waypoints: true,
719                });
720                self.write_component_ignore_entity_dead(entity, waypoint);
721                self.write_component_ignore_entity_dead(entity, comp::Pos(waypoint.get_pos()));
722                self.write_component_ignore_entity_dead(entity, comp::Vel(Vec3::zero()));
723                // TODO: We probably want to increment the existing force update counter since
724                // it is added in initialized_character (to be robust we can also insert it if
725                // it doesn't exist)
726                self.write_component_ignore_entity_dead(entity, comp::ForceUpdate::forced());
727            }
728
729            if let Some(map_marker) = map_marker {
730                self.write_component_ignore_entity_dead(entity, map_marker);
731            }
732
733            let player_pos = self.ecs().read_storage::<comp::Pos>().get(entity).copied();
734            if let Some(player_pos) = player_pos {
735                trace!(
736                    "Loading {} pets for player at pos {:?}",
737                    pets.len(),
738                    player_pos
739                );
740                let mut rng = rand::rng();
741
742                for (pet, body, stats) in pets {
743                    let ori = comp::Ori::from(Dir::random_2d(&mut rng));
744                    let pet_entity = self
745                        .create_npc(
746                            player_pos,
747                            ori,
748                            stats,
749                            comp::SkillSet::default(),
750                            Some(comp::Health::new(body)),
751                            Poise::new(body),
752                            Inventory::with_loadout(
753                                LoadoutBuilder::from_default(&body).build(),
754                                body,
755                            ),
756                            body,
757                            comp::Scale(1.0),
758                        )
759                        .with(comp::Vel(Vec3::new(0.0, 0.0, 0.0)))
760                        .build();
761
762                    restore_pet(self.ecs(), pet_entity, entity, pet);
763                }
764            } else {
765                error!("Player has no pos, cannot load {} pets", pets.len());
766            }
767
768            let settings = self.ecs().read_resource::<Settings>();
769            let mut char_battle_mode = settings.gameplay.battle_mode.default_mode();
770            let presences = self.ecs().read_storage::<Presence>();
771            let presence = presences.get(entity);
772            if let Some(Presence {
773                kind: PresenceKind::Character(char_id),
774                ..
775            }) = presence
776            {
777                let battlemode_buffer = self.ecs().fetch::<BattleModeBuffer>();
778                let mut players = self.ecs().write_storage::<comp::Player>();
779                if let Some(mut player_info) = players.get_mut(entity) {
780                    if let Some((mode, change)) = battlemode_buffer.get(char_id) {
781                        char_battle_mode = *mode;
782                        player_info.last_battlemode_change = Some(*change);
783                    } else {
784                        // TODO: this sounds related to handle_exit_ingame? Actually, sounds like
785                        // trying to place character specific info on the `Player` component. TODO
786                        // document component better.
787                        // FIXME:
788                        // ???
789                        //
790                        // This probably shouldn't exist,
791                        // but without this code, character gets battle_mode from
792                        // another character on this account.
793                        player_info.last_battlemode_change = None;
794                    }
795
796                    player_info.battle_mode = char_battle_mode;
797                }
798            }
799
800            if self
801                .ecs()
802                .read_component::<Client>()
803                .get(entity)
804                .is_some_and(|client| client.client_type.emit_login_events())
805            {
806                // Notify clients of a player list update
807                self.notify_players(ServerGeneral::PlayerListUpdate(
808                    PlayerListUpdate::SelectedCharacter(player_uid, CharacterInfo {
809                        name,
810                        gender,
811                        battle_mode: char_battle_mode,
812                    }),
813                ));
814            }
815        }
816
817        Ok(())
818    }
819
820    fn validate_chat_msg(
821        &self,
822        entity: EcsEntity,
823        chat_type: &comp::ChatType<comp::Group>,
824        msg: &Content,
825        from_client: bool,
826    ) -> bool {
827        let mut automod = self.ecs().write_resource::<AutoMod>();
828        let client = self.ecs().read_storage::<Client>();
829        let player = self.ecs().read_storage::<Player>();
830        let Some(client) = client.get(entity) else {
831            return true;
832        };
833        let Some(player) = player.get(entity) else {
834            return true;
835        };
836
837        // Don't permit players to send non-plain content sent from their clients...
838        // yet. TODO: Eventually, it would be nice for players to be able to
839        // send messages that get localised on their client!
840        let Some(msg) = msg.as_plain() else {
841            return !from_client;
842        };
843
844        match automod.validate_chat_msg(
845            player.uuid(),
846            self.ecs()
847                .read_storage::<comp::Admin>()
848                .get(entity)
849                .map(|a| a.0),
850            Instant::now(),
851            chat_type,
852            msg,
853        ) {
854            Ok(note) => {
855                if let Some(note) = note {
856                    let _ = client.send(ServerGeneral::server_msg(
857                        ChatType::CommandInfo,
858                        Content::Plain(format!("{}", note)),
859                    ));
860                }
861                true
862            },
863            Err(err) => {
864                let _ = client.send(ServerGeneral::server_msg(
865                    ChatType::CommandError,
866                    Content::Plain(format!("{}", err)),
867                ));
868                false
869            },
870        }
871    }
872
873    /// Send the chat message to the proper players. Say and region are limited
874    /// by location. Faction and group are limited by component.
875    fn send_chat(&self, msg: comp::UnresolvedChatMsg, from_client: bool) {
876        let ecs = self.ecs();
877        let is_within =
878            |target, a: &comp::Pos, b: &comp::Pos| a.0.distance_squared(b.0) < target * target;
879
880        let group_manager = ecs.read_resource::<comp::group::GroupManager>();
881        let chat_exporter = ecs.read_resource::<ChatExporter>();
882
883        let group_info = msg.get_group().and_then(|g| group_manager.group_info(*g));
884
885        if let Some(exported_message) = ChatExporter::generate(&msg, ecs) {
886            chat_exporter.send(exported_message);
887        }
888
889        let resolved_msg = msg
890            .clone()
891            .map_group(|_| group_info.map_or_else(|| "???".to_string(), |i| i.name.clone()));
892
893        let id_maps = ecs.read_resource::<IdMaps>();
894        let entity_from_uid = |uid| id_maps.uid_entity(uid);
895
896        if msg.chat_type.uid().is_none_or(|sender| {
897            entity_from_uid(sender).is_some_and(|e| {
898                self.validate_chat_msg(e, &msg.chat_type, msg.content(), from_client)
899            })
900        }) {
901            match &msg.chat_type {
902                comp::ChatType::Offline(_)
903                | comp::ChatType::CommandInfo
904                | comp::ChatType::CommandError
905                | comp::ChatType::Meta
906                | comp::ChatType::World(_) => {
907                    self.notify_players(ServerGeneral::ChatMsg(resolved_msg))
908                },
909                comp::ChatType::Online(u) => {
910                    for (client, uid) in
911                        (&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>()).join()
912                    {
913                        if uid != u {
914                            client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
915                        }
916                    }
917                },
918                &comp::ChatType::Tell(from, to) => {
919                    let clients = ecs.read_storage::<Client>();
920                    if let Some(from_client) = entity_from_uid(from).and_then(|e| clients.get(e)) {
921                        from_client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
922                    }
923                    if let Some(to_client) = entity_from_uid(to).and_then(|e| clients.get(e)) {
924                        to_client.send_fallible(ServerGeneral::ChatMsg(resolved_msg));
925                    }
926                },
927                comp::ChatType::Kill(kill_source, uid) => {
928                    let clients = ecs.read_storage::<Client>();
929                    let clients_count = clients.count();
930                    // Avoid chat spam, send kill message only to group or nearby players if a
931                    // certain amount of clients are online
932                    if clients_count
933                        > ecs
934                            .fetch::<Settings>()
935                            .max_player_for_kill_broadcast
936                            .unwrap_or_default()
937                    {
938                        // Send kill message to the dead player's group
939                        let killed_entity = entity_from_uid(*uid);
940                        let groups = ecs.read_storage::<Group>();
941                        let killed_group = killed_entity.and_then(|e| groups.get(e));
942                        if let Some(g) = &killed_group {
943                            send_to_group(g, ecs, &resolved_msg);
944                        }
945
946                        // Send kill message to nearby players that aren't part of the deceased's
947                        // group
948                        let positions = ecs.read_storage::<comp::Pos>();
949                        if let Some(died_player_pos) = killed_entity.and_then(|e| positions.get(e))
950                        {
951                            for (ent, client, pos) in
952                                (&*ecs.entities(), &clients, &positions).join()
953                            {
954                                let client_group = groups.get(ent);
955                                let is_different_group =
956                                    !(killed_group == client_group && client_group.is_some());
957                                if is_within(comp::ChatMsg::SAY_DISTANCE, pos, died_player_pos)
958                                    && is_different_group
959                                {
960                                    client.send_fallible(ServerGeneral::ChatMsg(
961                                        resolved_msg.clone(),
962                                    ));
963                                }
964                            }
965                        }
966                    } else {
967                        self.notify_players(ServerGeneral::server_msg(
968                            comp::ChatType::Kill(kill_source.clone(), *uid),
969                            msg.into_content(),
970                        ))
971                    }
972                },
973                comp::ChatType::Say(uid) => {
974                    let entity_opt = entity_from_uid(*uid);
975
976                    let positions = ecs.read_storage::<comp::Pos>();
977                    if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
978                        for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
979                            if is_within(comp::ChatMsg::SAY_DISTANCE, pos, speaker_pos) {
980                                client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
981                            }
982                        }
983                    }
984                },
985                comp::ChatType::Region(uid) => {
986                    let entity_opt = entity_from_uid(*uid);
987
988                    let positions = ecs.read_storage::<comp::Pos>();
989                    if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
990                        for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
991                            if is_within(comp::ChatMsg::REGION_DISTANCE, pos, speaker_pos) {
992                                client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
993                            }
994                        }
995                    }
996                },
997                comp::ChatType::Npc(uid) => {
998                    let entity_opt = entity_from_uid(*uid);
999
1000                    let positions = ecs.read_storage::<comp::Pos>();
1001                    if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
1002                        for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
1003                            if is_within(comp::ChatMsg::NPC_DISTANCE, pos, speaker_pos) {
1004                                client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
1005                            }
1006                        }
1007                    }
1008                },
1009                comp::ChatType::NpcSay(uid) => {
1010                    let entity_opt = entity_from_uid(*uid);
1011
1012                    let positions = ecs.read_storage::<comp::Pos>();
1013                    if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
1014                        for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
1015                            if is_within(comp::ChatMsg::NPC_SAY_DISTANCE, pos, speaker_pos) {
1016                                client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
1017                            }
1018                        }
1019                    }
1020                },
1021                &comp::ChatType::NpcTell(from, to) => {
1022                    let clients = ecs.read_storage::<Client>();
1023                    if let Some(from_client) = entity_from_uid(from).and_then(|e| clients.get(e)) {
1024                        from_client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
1025                    }
1026                    if let Some(to_client) = entity_from_uid(to).and_then(|e| clients.get(e)) {
1027                        to_client.send_fallible(ServerGeneral::ChatMsg(resolved_msg));
1028                    }
1029                },
1030                comp::ChatType::FactionMeta(s) | comp::ChatType::Faction(_, s) => {
1031                    for (client, faction) in (
1032                        &ecs.read_storage::<Client>(),
1033                        &ecs.read_storage::<comp::Faction>(),
1034                    )
1035                        .join()
1036                    {
1037                        if s == &faction.0 {
1038                            client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
1039                        }
1040                    }
1041                },
1042                comp::ChatType::Group(from, g) => {
1043                    if group_info.is_none() {
1044                        // Group not found, reply with command error
1045                        // This should usually NEVER happen since now it is checked whether the
1046                        // sender is still in the group upon emitting the message (TODO: Can this be
1047                        // triggered if the message is sent in the same tick as the sender is
1048                        // removed from the group?)
1049
1050                        let reply = comp::ChatType::CommandError
1051                            .into_msg(Content::localized("command-message-group-missing"));
1052
1053                        let clients = ecs.read_storage::<Client>();
1054                        if let Some(client) =
1055                            entity_from_uid(*from).and_then(|entity| clients.get(entity))
1056                        {
1057                            client.send_fallible(ServerGeneral::ChatMsg(reply));
1058                        }
1059                    } else {
1060                        send_to_group(g, ecs, &resolved_msg);
1061                    }
1062                },
1063                comp::ChatType::GroupMeta(g) => {
1064                    send_to_group(g, ecs, &resolved_msg);
1065                },
1066            }
1067        }
1068    }
1069
1070    /// Sends the message to all connected clients
1071    fn notify_players(&self, msg: ServerGeneral) {
1072        let mut msg = Some(msg);
1073        let mut lazy_msg = None;
1074        for (client, _) in (
1075            &self.ecs().read_storage::<Client>(),
1076            &self.ecs().read_storage::<comp::Player>(),
1077        )
1078            .join()
1079        {
1080            if let Some(msg) = msg.take() {
1081                lazy_msg = Some(client.prepare(msg));
1082            }
1083            lazy_msg.as_ref().map(|msg| client.send_prepared(msg));
1084        }
1085    }
1086
1087    /// Sends the message to all clients playing in game
1088    fn notify_in_game_clients(&self, msg: ServerGeneral) {
1089        let mut msg = Some(msg);
1090        let mut lazy_msg = None;
1091        for (client, _) in (
1092            &mut self.ecs().write_storage::<Client>(),
1093            &self.ecs().read_storage::<Presence>(),
1094        )
1095            .join()
1096        {
1097            if let Some(msg) = msg.take() {
1098                lazy_msg = Some(client.prepare(msg));
1099            }
1100            lazy_msg.as_ref().map(|msg| client.send_prepared(msg));
1101        }
1102    }
1103
1104    fn link<L: Link>(&mut self, link: L) -> Result<(), L::Error> {
1105        let linker = LinkHandle::from_link(link);
1106
1107        L::create(&linker, &mut self.ecs().system_data())?;
1108
1109        self.ecs_mut()
1110            .entry::<Vec<LinkHandle<L>>>()
1111            .or_insert_with(Vec::new)
1112            .push(linker);
1113
1114        Ok(())
1115    }
1116
1117    fn maintain_links(&mut self) {
1118        fn maintain_link<L: Link>(state: &State) {
1119            if let Some(mut handles) = state.ecs().try_fetch_mut::<Vec<LinkHandle<L>>>() {
1120                let mut persist_data = None;
1121                handles.retain(|link| {
1122                    if L::persist(
1123                        link,
1124                        persist_data.get_or_insert_with(|| state.ecs().system_data()),
1125                    ) {
1126                        true
1127                    } else {
1128                        // Make sure to drop persist data before running deletion to avoid potential
1129                        // access violations
1130                        persist_data.take();
1131                        L::delete(link, &mut state.ecs().system_data());
1132                        false
1133                    }
1134                });
1135            }
1136        }
1137
1138        maintain_link::<Mounting>(self);
1139        maintain_link::<VolumeMounting>(self);
1140        maintain_link::<Tethered>(self);
1141        maintain_link::<Interaction>(self);
1142    }
1143
1144    fn delete_entity_recorded(
1145        &mut self,
1146        entity: EcsEntity,
1147    ) -> Result<(), specs::error::WrongGeneration> {
1148        // NOTE: both this and handle_exit_ingame call delete_entity_common, so cleanup
1149        // added here may need to be duplicated in handle_exit_ingame (depending
1150        // on its nature).
1151
1152        // Remove entity from a group if they are in one.
1153        {
1154            let clients = self.ecs().read_storage::<Client>();
1155            let uids = self.ecs().read_storage::<Uid>();
1156            let mut group_manager = self.ecs().write_resource::<comp::group::GroupManager>();
1157            let map_markers = self.ecs().read_storage::<comp::MapMarker>();
1158            group_manager.entity_deleted(
1159                entity,
1160                &mut self.ecs().write_storage(),
1161                &self.ecs().read_storage(),
1162                &uids,
1163                &self.ecs().entities(),
1164                &mut |entity, group_change| {
1165                    clients
1166                        .get(entity)
1167                        .and_then(|c| {
1168                            group_change
1169                                .try_map_ref(|e| uids.get(*e).copied())
1170                                .map(|g| (g, c))
1171                        })
1172                        .map(|(g, c)| {
1173                            update_map_markers(&map_markers, &uids, c, &group_change);
1174                            c.send_fallible(ServerGeneral::GroupUpdate(g));
1175                        });
1176                },
1177            );
1178        }
1179
1180        // Cancel extant trades
1181        events::shared::cancel_trades_for(self, entity);
1182
1183        // NOTE: We expect that these 3 components are never removed from an entity (nor
1184        // mutated) (at least not without updating the relevant mappings)!
1185        let maybe_uid = self.read_component_copied::<Uid>(entity);
1186        let (maybe_character, sync_me) = self
1187            .read_storage::<Presence>()
1188            .get(entity)
1189            .map(|p| (p.kind.character_id(), p.kind.sync_me()))
1190            .unzip();
1191        let maybe_rtsim = self.read_component_copied::<RtSimEntity>(entity);
1192
1193        self.mut_resource::<IdMaps>().remove_entity(
1194            Some(entity),
1195            maybe_uid,
1196            maybe_character.flatten(),
1197            maybe_rtsim,
1198        );
1199
1200        delete_entity_common(self, entity, maybe_uid, sync_me.unwrap_or(true))
1201    }
1202
1203    fn entity_as_actor(&self, entity: EcsEntity) -> Option<Actor> {
1204        if let Some(rtsim_entity) = self
1205            .ecs()
1206            .read_storage::<RtSimEntity>()
1207            .get(entity)
1208            .copied()
1209        {
1210            Some(Actor::Npc(rtsim_entity))
1211        } else if let Some(PresenceKind::Character(character)) = self
1212            .ecs()
1213            .read_storage::<Presence>()
1214            .get(entity)
1215            .map(|p| p.kind)
1216        {
1217            Some(Actor::Character(character))
1218        } else {
1219            None
1220        }
1221    }
1222
1223    fn position_mut<T>(
1224        &mut self,
1225        entity: EcsEntity,
1226        dismount_volume: bool,
1227        f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T,
1228    ) -> Result<T, Content> {
1229        let ecs = self.ecs_mut();
1230        position_mut(
1231            entity,
1232            dismount_volume,
1233            f,
1234            &ecs.read_resource(),
1235            &mut ecs.write_storage(),
1236            ecs.write_storage(),
1237            ecs.write_storage(),
1238            ecs.read_storage(),
1239            ecs.read_storage(),
1240            ecs.read_storage(),
1241        )
1242    }
1243
1244    fn position_mut_reposition<T>(
1245        &mut self,
1246        entity: EcsEntity,
1247        dismount_volume: bool,
1248        f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T,
1249        needs_ground: bool,
1250        modify_waypoints: bool,
1251    ) -> Result<T, Content> {
1252        self.position_mut(entity, dismount_volume, f).inspect(|_| {
1253            self.write_component_ignore_entity_dead(entity, RepositionToFreeSpace {
1254                needs_ground,
1255                modify_waypoints,
1256            });
1257        })
1258    }
1259}
1260
1261pub fn position_mut<T>(
1262    entity: EcsEntity,
1263    dismount_volume: bool,
1264    f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T,
1265    id_maps: &IdMaps,
1266    is_volume_riders: &mut WriteStorage<Is<VolumeRider>>,
1267    mut positions: impl GenericWriteStorage<Component = comp::Pos>,
1268    mut force_updates: impl GenericWriteStorage<Component = comp::ForceUpdate>,
1269    is_riders: impl GenericReadStorage<Component = Is<Rider>>,
1270    presences: impl GenericReadStorage<Component = Presence>,
1271    clients: impl GenericReadStorage<Component = Client>,
1272) -> Result<T, Content> {
1273    if dismount_volume {
1274        is_volume_riders.remove(entity);
1275    }
1276
1277    let entity = is_riders
1278        .get(entity)
1279        .and_then(|is_rider| id_maps.uid_entity(is_rider.mount))
1280        .map(Ok)
1281        .or_else(|| {
1282            is_volume_riders.get(entity).and_then(|volume_rider| {
1283                Some(match volume_rider.pos.kind {
1284                    common::mounting::Volume::Terrain => {
1285                        Err(Content::Plain("Tried to move the world.".to_string()))
1286                    },
1287                    common::mounting::Volume::Entity(uid) => Ok(id_maps.uid_entity(uid)?),
1288                })
1289            })
1290        })
1291        .unwrap_or(Ok(entity))?;
1292
1293    let mut maybe_pos = None;
1294
1295    let res = positions
1296        .get_mut(entity)
1297        .map(|pos| {
1298            let res = f(pos);
1299            maybe_pos = Some(pos.0);
1300            res
1301        })
1302        .ok_or(Content::localized_with_args(
1303            "command-position-unavailable",
1304            [("target", "entity")],
1305        ));
1306
1307    if let Some(pos) = maybe_pos {
1308        if presences
1309            .get(entity)
1310            .map(|presence| presence.kind == PresenceKind::Spectator)
1311            .unwrap_or(false)
1312        {
1313            clients.get(entity).map(|client| {
1314                client.send_fallible(ServerGeneral::SpectatePosition(pos));
1315            });
1316        } else {
1317            force_updates
1318                .get_mut(entity)
1319                .map(|force_update| force_update.update());
1320        }
1321    }
1322
1323    res
1324}
1325
1326fn send_to_group(g: &Group, ecs: &specs::World, msg: &comp::ChatMsg) {
1327    for (client, group) in (&ecs.read_storage::<Client>(), &ecs.read_storage::<Group>()).join() {
1328        if g == group {
1329            client.send_fallible(ServerGeneral::ChatMsg(msg.clone()));
1330        }
1331    }
1332}
1333
1334/// This should only be called from `handle_exit_ingame` and
1335/// `delete_entity_recorded`!!!!!!!
1336pub(crate) fn delete_entity_common(
1337    state: &mut State,
1338    entity: EcsEntity,
1339    maybe_uid: Option<Uid>,
1340    sync_me: bool,
1341) -> Result<(), specs::error::WrongGeneration> {
1342    let maybe_pos = state.read_component_copied::<comp::Pos>(entity);
1343
1344    // Delete entity
1345    let result = state.ecs_mut().delete_entity(entity);
1346
1347    if result.is_ok() {
1348        let region_map = state.mut_resource::<common::region::RegionMap>();
1349        let region_key = region_map.entity_deleted(entity);
1350        // Note: Adding the `Uid` to the deleted list when exiting "in-game" relies on
1351        // the client not being able to immediately re-enter the game in the
1352        // same tick (since we could then mix up the ordering of things and
1353        // tell other clients to forget the new entity).
1354        //
1355        // The client will ignore requests to delete its own entity that are triggered
1356        // by this.
1357        if let Some(uid) = maybe_uid {
1358            if let Some(region_key) = region_key {
1359                state
1360                    .mut_resource::<DeletedEntities>()
1361                    .record_deleted_entity(uid, region_key);
1362            // If there is a position and sync_me is true, but the entity is not
1363            // in a region, something might be wrong.
1364            } else if sync_me && let Some(pos) = maybe_pos {
1365                // Don't panic if the entity wasn't found in a region, maybe it was just created
1366                // and then deleted before the region manager had a chance to assign it a region
1367                warn!(
1368                    ?uid,
1369                    ?pos,
1370                    "Failed to find region containing entity during entity deletion, assuming it \
1371                     wasn't sent to any clients and so deletion doesn't need to be recorded for \
1372                     sync purposes"
1373                );
1374            }
1375        } else {
1376            // For now we expect all entities have a Uid component. If this is changed, the
1377            // RegionMap needs to account for presence of Uid when deciding what should be
1378            // tracked.
1379            error!("Deleting entity without Uid component");
1380        }
1381    }
1382    result
1383}