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