veloren_server/events/
player.rs

1use super::Event;
2use crate::{
3    BattleModeBuffer, Server, client::Client, metrics::PlayerMetrics,
4    persistence::character_updater::CharacterUpdater, settings::banlist::NormalizedIpAddr,
5    state_ext::StateExt,
6};
7use common::{
8    comp::{self, Content, Presence, PresenceKind, group, pet::is_tameable},
9    event::{DeleteCharacterEvent, PossessEvent, SetBattleModeEvent},
10    resources::Time,
11    uid::{IdMaps, Uid},
12};
13use common_base::span;
14use common_net::msg::{PlayerListUpdate, ServerGeneral};
15use common_state::State;
16use hashbrown::HashSet;
17use specs::{Builder, Entity as EcsEntity, Join, WorldExt};
18use tracing::{Instrument, debug, error, trace, warn};
19
20pub fn handle_character_delete(server: &mut Server, ev: DeleteCharacterEvent) {
21    // Can't process a character delete for a player that has an in-game presence,
22    // so kick them out before processing the delete.
23    // NOTE: This relies on StateExt::handle_initialize_character adding the
24    // Presence component when a character is initialized to detect whether a client
25    // is in-game.
26    let has_presence = {
27        let presences = server.state.ecs().read_storage::<Presence>();
28        presences.get(ev.entity).is_some()
29    };
30    if has_presence {
31        warn!(
32            ?ev.requesting_player_uuid,
33            ?ev.character_id,
34            "Character delete received while in-game, disconnecting client."
35        );
36        handle_exit_ingame(server, ev.entity, true);
37    }
38
39    let mut updater = server.state.ecs().fetch_mut::<CharacterUpdater>();
40    updater.queue_character_deletion(ev.requesting_player_uuid, ev.character_id);
41}
42
43pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity, skip_persistence: bool) {
44    span!(_guard, "handle_exit_ingame");
45    let state = server.state_mut();
46
47    // Sync the player's character data to the database. This must be done before
48    // removing any components from the entity
49    let entity = if !skip_persistence {
50        persist_entity(state, entity)
51    } else {
52        entity
53    };
54
55    // Create new entity with just `Client`, `Uid`, `Player`, `Admin`, `Group`
56    // components.
57    //
58    // Easier than checking and removing all other known components.
59    //
60    // Also, allows clients to not update their Uid based references to this
61    // client (e.g. for this specific client's knowledge of its own Uid and for
62    // groups since exiting in-game does not affect group membership)
63    //
64    // Note: If other `ServerEvent`s are referring to this entity they will be
65    // disrupted.
66
67    // Cancel trades here since we don't use `delete_entity_recorded` and we
68    // remove `Uid` below.
69    super::trade::cancel_trades_for(state, entity);
70
71    let maybe_group = state.read_component_copied::<group::Group>(entity);
72    let maybe_admin = state.delete_component::<comp::Admin>(entity);
73    // Not sure if we still need to actually remove the Uid or if the group
74    // logic below relies on this...
75    let maybe_uid = state.delete_component::<Uid>(entity);
76
77    if let Some(client) = state.delete_component::<Client>(entity)
78        && let Some(uid) = maybe_uid
79        && let Some(player) = state.delete_component::<comp::Player>(entity)
80    {
81        // Tell client its request was successful
82        client.send_fallible(ServerGeneral::ExitInGameSuccess);
83
84        if client.client_type.emit_login_events() {
85            state.notify_players(ServerGeneral::PlayerListUpdate(
86                PlayerListUpdate::ExitCharacter(uid),
87            ));
88        }
89
90        let new_entity = state
91            .ecs_mut()
92            .create_entity()
93            .with(client)
94            .with(player)
95            // Preserve group component if present
96            .maybe_with(maybe_group)
97            // Preserve admin component if present
98            .maybe_with(maybe_admin)
99            .with(uid)
100            .build();
101
102        // Ensure IdMaps maps this uid to the new entity.
103        state.mut_resource::<IdMaps>().remap_entity(uid, new_entity);
104
105        let ecs = state.ecs();
106        // Note, we use `delete_entity_common` directly to avoid
107        // `delete_entity_recorded` from making any changes to the group.
108        if let Some(group) = maybe_group {
109            let mut group_manager = ecs.write_resource::<group::GroupManager>();
110            if group_manager
111                .group_info(group)
112                .map(|info| info.leader == entity)
113                .unwrap_or(false)
114            {
115                group_manager.assign_leader(
116                    new_entity,
117                    &ecs.read_storage(),
118                    &ecs.entities(),
119                    &ecs.read_storage(),
120                    &ecs.read_storage(),
121                    // Nothing actually changing since Uid is transferred
122                    |_, _| {},
123                );
124            }
125        }
126
127        // delete_entity_recorded` is not used so we don't need to worry aobut
128        // group restructuring when deleting this entity.
129    } else {
130        error!("handle_exit_ingame called with entity that is missing expected components");
131    }
132
133    let (maybe_character, sync_me) = state
134        .read_storage::<Presence>()
135        .get(entity)
136        .map(|p| (p.kind.character_id(), p.kind.sync_me()))
137        .unzip();
138    let maybe_rtsim = state.read_component_copied::<common::rtsim::RtSimEntity>(entity);
139    state.mut_resource::<IdMaps>().remove_entity(
140        Some(entity),
141        None, // Uid re-mapped, we don't want to remove the mapping
142        maybe_character.flatten(),
143        maybe_rtsim,
144    );
145
146    // If the character had a RtSim id (possibly from possesing an rtsim entity),
147    // make rtsim aware that this entity can now be respawned.
148    #[cfg(feature = "worldgen")]
149    if let Some(rtsim_entity) = maybe_rtsim {
150        let world = state.ecs().read_resource::<std::sync::Arc<world::World>>();
151        let index = state.ecs().read_resource::<world::index::IndexOwned>();
152        let pos = state.read_component_copied::<comp::Pos>(entity);
153        state
154            .ecs()
155            .write_resource::<crate::rtsim::RtSim>()
156            .hook_rtsim_actor_death(
157                &world,
158                index.as_index_ref(),
159                common::rtsim::Actor::Npc(rtsim_entity.0),
160                pos.map(|p| p.0),
161                None,
162            );
163    }
164
165    // We don't want to use delete_entity_recorded since we are transfering the
166    // Uid to a new entity (and e.g. don't want it to be unmapped).
167    //
168    // Delete old entity
169    if let Err(e) =
170        crate::state_ext::delete_entity_common(state, entity, maybe_uid, sync_me.unwrap_or(true))
171    {
172        error!(
173            ?e,
174            ?entity,
175            "Failed to delete entity when removing character"
176        );
177    }
178}
179
180fn get_reason_str(reason: &comp::DisconnectReason) -> &str {
181    match reason {
182        comp::DisconnectReason::Timeout => "timeout",
183        comp::DisconnectReason::NetworkError => "network_error",
184        comp::DisconnectReason::NewerLogin => "newer_login",
185        comp::DisconnectReason::Kicked => "kicked",
186        comp::DisconnectReason::ClientRequested => "client_requested",
187        comp::DisconnectReason::InvalidClientType => "invalid_client_type",
188    }
189}
190
191#[must_use]
192pub fn handle_client_disconnect(
193    server: &mut Server,
194    mut entity: EcsEntity,
195    reason: comp::DisconnectReason,
196    skip_persistence: bool,
197    already_disconnected_clients: &mut HashSet<EcsEntity>,
198) -> Option<Event> {
199    span!(_guard, "handle_client_disconnect");
200
201    // NOTE: There are not and likely will not be a way to safeguard against
202    // receiving multiple `ServerEvent::ClientDisconnect` messages in a tick
203    // intended for the same client, so we track if a disconnect has already
204    // been received and skip logging certain errors if there was
205    // already a disconnect event for this entity.
206    let already_disconnected = !already_disconnected_clients.insert(entity);
207
208    let mut emit_logoff_event = true;
209    let mut disconnected_event = None;
210
211    // Entity deleted below and persist_entity doesn't require a `Client` component,
212    // so we can just remove the Client component to get ownership of the
213    // participant.
214    if let Some(client) = server
215        .state()
216        .ecs()
217        .write_storage::<Client>()
218        .remove(entity)
219    {
220        server
221            .state()
222            .ecs()
223            .read_resource::<PlayerMetrics>()
224            .clients_disconnected
225            .with_label_values(&[get_reason_str(&reason)])
226            .inc();
227
228        if let Some(player) = server
229            .state()
230            .ecs()
231            .read_storage::<comp::Player>()
232            .get(entity)
233            && let Some(connect_addr) = client.connected_from_addr().socket_addr()
234        {
235            server
236                .state()
237                .ecs()
238                .write_resource::<crate::RecentClientIPs>()
239                .last_addrs
240                .insert(player.uuid(), NormalizedIpAddr::from(connect_addr.ip()));
241        }
242
243        if let Some(participant) = client.participant {
244            let pid = participant.remote_pid();
245            server.runtime.spawn(
246                async {
247                    let now = std::time::Instant::now();
248                    debug!("Start handle disconnect of client");
249                    if let Err(e) = participant.disconnect().await {
250                        debug!(
251                            ?e,
252                            "Error when disconnecting client, maybe the pipe already broke"
253                        );
254                    };
255                    trace!("finished disconnect");
256                    let elapsed = now.elapsed();
257                    if elapsed.as_millis() > 100 {
258                        warn!(?elapsed, "disconnecting took quite long");
259                    } else {
260                        debug!(?elapsed, "disconnecting took");
261                    }
262                }
263                .instrument(tracing::debug_span!(
264                    "client_disconnect",
265                    ?pid,
266                    ?entity,
267                    ?reason,
268                )),
269            );
270        } else if !already_disconnected {
271            error!("handle_client_disconnect called for entity without client component");
272        }
273
274        emit_logoff_event = client.client_type.emit_login_events();
275        disconnected_event = Some(Event::ClientDisconnected { entity });
276    }
277
278    let state = server.state_mut();
279
280    // Tell other clients to remove from player list
281    // And send a disconnected message
282    if let (Some(uid), Some(_)) = (
283        state.read_storage::<Uid>().get(entity),
284        state.read_storage::<comp::Player>().get(entity),
285    ) && emit_logoff_event
286    {
287        state.notify_players(ServerGeneral::server_msg(
288            comp::ChatType::Offline(*uid),
289            Content::Plain("".to_string()),
290        ));
291
292        state.notify_players(ServerGeneral::PlayerListUpdate(PlayerListUpdate::Remove(
293            *uid,
294        )));
295    }
296
297    // Sync the player's character data to the database
298    if !skip_persistence {
299        entity = persist_entity(state, entity);
300    }
301
302    // Delete client entity
303    if let Err(e) = server.state.delete_entity_recorded(entity)
304        && !already_disconnected
305    {
306        error!(?e, ?entity, "Failed to delete disconnected client");
307    }
308
309    disconnected_event
310}
311
312/// When a player logs out, their data is queued for persistence in the next
313/// tick of the persistence batch update unless the character logging out is
314/// dead and has hardcore enabled, in which case the character is deleted
315/// instead of being persisted. The player will be temporarily unable to log in
316/// during this period to avoid the race condition of their login fetching their
317/// old data and overwriting the data saved here.
318///
319/// This function is also used by the Transform event and MUST NOT assume that
320/// the persisting entity is deleted afterwards. It is however safe to assume
321/// that this function will not be called twice on an entity with the same
322/// character id.
323pub(super) fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
324    // NOTE: `Client` component may already be removed by the caller to close the
325    // connection. Don't depend on it here!
326    if let (
327        Some(presence),
328        Some(skill_set),
329        Some(inventory),
330        Some(active_abilities),
331        Some(player_uid),
332        Some(player_info),
333        mut character_updater,
334        mut battlemode_buffer,
335    ) = (
336        state.read_storage::<Presence>().get(entity),
337        state.read_storage::<comp::SkillSet>().get(entity),
338        state.read_storage::<comp::Inventory>().get(entity),
339        state
340            .read_storage::<comp::ability::ActiveAbilities>()
341            .get(entity),
342        state.read_storage::<Uid>().get(entity),
343        state.read_storage::<comp::Player>().get(entity),
344        state.ecs().fetch_mut::<CharacterUpdater>(),
345        state.ecs().fetch_mut::<BattleModeBuffer>(),
346    ) {
347        match presence.kind {
348            PresenceKind::LoadingCharacter(_char_id) => {
349                error!(
350                    "Unexpected state when persist_entity is called! Some of the components \
351                     required above should only be present after a character is loaded!"
352                );
353            },
354            PresenceKind::Character(char_id) => {
355                if state.read_storage::<comp::Hardcore>().get(entity).is_some()
356                    && state
357                        .read_storage::<comp::Health>()
358                        .get(entity)
359                        .is_some_and(|health| health.is_dead)
360                {
361                    // Delete dead hardcore characters instead of persisting
362                    character_updater
363                        .queue_character_deletion(player_info.uuid().to_string(), char_id);
364                } else {
365                    let waypoint = state
366                        .ecs()
367                        .read_storage::<comp::Waypoint>()
368                        .get(entity)
369                        .cloned();
370                    let map_marker = state
371                        .ecs()
372                        .read_storage::<comp::MapMarker>()
373                        .get(entity)
374                        .cloned();
375                    // Store last battle mode change
376                    if let Some(change) = player_info.last_battlemode_change {
377                        let mode = player_info.battle_mode;
378                        let save = (mode, change);
379                        battlemode_buffer.push(char_id, save);
380                    }
381
382                    // Get player's pets
383                    let alignments = state.ecs().read_storage::<comp::Alignment>();
384                    let bodies = state.ecs().read_storage::<comp::Body>();
385                    let stats = state.ecs().read_storage::<comp::Stats>();
386                    let pets = state.ecs().read_storage::<comp::Pet>();
387                    let pets = (&alignments, &bodies, &stats, &pets)
388                        .join()
389                        .filter_map(|(alignment, body, stats, pet)| match alignment {
390                            // Don't try to persist non-tameable pets (likely spawned
391                            // using /spawn) since there isn't any code to handle
392                            // persisting them
393                            common::comp::Alignment::Owned(pet_owner)
394                                if pet_owner == player_uid && is_tameable(body) =>
395                            {
396                                Some(((*pet).clone(), *body, stats.clone()))
397                            },
398                            _ => None,
399                        })
400                        .collect();
401
402                    character_updater.add_pending_logout_update((
403                        char_id,
404                        skill_set.clone(),
405                        inventory.clone(),
406                        pets,
407                        waypoint,
408                        active_abilities.clone(),
409                        map_marker,
410                    ));
411                }
412            },
413            PresenceKind::Spectator => { /* Do nothing, spectators do not need persisting */ },
414            PresenceKind::Possessor => { /* Do nothing, possessor's are not persisted */ },
415        };
416    }
417
418    entity
419}
420
421/// FIXME: This code is dangerous and needs to be refactored.  We can't just
422/// comment it out, but it needs to be fixed for a variety of reasons.  Get rid
423/// of this ASAP!
424pub fn handle_possess(
425    server: &mut Server,
426    PossessEvent(possessor_uid, possessee_uid): PossessEvent,
427) {
428    use crate::presence::RegionSubscription;
429    use common::{
430        comp::{Inventory, inventory::slot::EquipSlot, item, slot::Slot},
431        region::RegionMap,
432    };
433    use common_net::sync::WorldSyncExt;
434
435    let state = server.state_mut();
436    let mut delete_entity = None;
437
438    if let (Some(possessor), Some(possessee)) = (
439        state.ecs().entity_from_uid(possessor_uid),
440        state.ecs().entity_from_uid(possessee_uid),
441    ) {
442        // In this section we check various invariants and can return early if any of
443        // them are not met.
444        let new_presence = {
445            let ecs = state.ecs();
446            // Check that entities still exist
447            if !possessor.gen().is_alive()
448                || !ecs.is_alive(possessor)
449                || !possessee.gen().is_alive()
450                || !ecs.is_alive(possessee)
451            {
452                error!(
453                    "Error possessing! either the possessor entity or possessee entity no longer \
454                     exists"
455                );
456                return;
457            }
458
459            let clients = ecs.read_storage::<Client>();
460            let players = ecs.read_storage::<comp::Player>();
461            let presences = ecs.read_storage::<comp::Presence>();
462
463            if clients.contains(possessee) || players.contains(possessee) {
464                error!("Can't possess other players!");
465                return;
466            }
467
468            if !clients.contains(possessor) {
469                error!("Error posessing, no `Client` component on the possessor!");
470                return;
471            }
472
473            // Limit possessible entities to those in the client's subscribed regions (so
474            // that the entity already exists on the client, this reduces the
475            // amount of syncing edge cases to consider).
476            let subscriptions = ecs.read_storage::<RegionSubscription>();
477            let region_map = ecs.read_resource::<RegionMap>();
478            let possessee_in_subscribed_region = subscriptions
479                .get(possessor)
480                .iter()
481                .flat_map(|s| s.regions.iter())
482                .filter_map(|key| region_map.get(*key))
483                .any(|region| region.entities().contains(possessee.id()));
484            if !possessee_in_subscribed_region {
485                return;
486            }
487
488            if let Some(presence) = presences.get(possessor) {
489                delete_entity = match presence.kind {
490                    k @ (PresenceKind::LoadingCharacter(_) | PresenceKind::Spectator) => {
491                        error!(?k, "Unexpected presence kind for a possessor.");
492                        return;
493                    },
494                    PresenceKind::Possessor => None,
495                    // Since we call `persist_entity` below we will want to delete the entity (to
496                    // avoid item duplication).
497                    PresenceKind::Character(_) => Some(possessor),
498                };
499
500                Some(Presence {
501                    terrain_view_distance: presence.terrain_view_distance,
502                    entity_view_distance: presence.entity_view_distance,
503                    // This kind (rather than copying Character presence) prevents persistence
504                    // from overwriting original character info with stuff from the new character.
505                    kind: PresenceKind::Possessor,
506                    lossy_terrain_compression: presence.lossy_terrain_compression,
507                })
508            } else {
509                None
510            }
511
512            // No early returns allowed after this.
513        };
514
515        // Sync the player's character data to the database. This must be done before
516        // moving any components from the entity.
517        //
518        // NOTE: Below we delete old entity (if PresenceKind::Character) as if logging
519        // out. This is to prevent any potential for item duplication (although
520        // it would only be possible if the player could repossess their entity,
521        // hand off some items, and then crash the server in a particular time
522        // window, and only admins should have access to the item with this ability
523        // in the first place (though that isn't foolproof)). We could potentially fix
524        // this but it would require some tweaks to the CharacterUpdater code
525        // (to be able to deque the pending persistence request issued here if
526        // repossesing the original character), and it seems prudent to be more
527        // conservative with making changes there to support this feature.
528        let possessor = persist_entity(state, possessor);
529        let ecs = state.ecs();
530
531        let mut clients = ecs.write_storage::<Client>();
532
533        // Transfer client component. Note: we require this component for possession.
534        let client = clients
535            .remove(possessor)
536            .expect("Checked client component was present above!");
537        client.send_fallible(ServerGeneral::SetPlayerEntity(possessee_uid));
538        let emit_player_list_events = client.client_type.emit_login_events();
539        // Note: we check that the `possessor` and `possessee` entities exist above, so
540        // this should never panic.
541        clients
542            .insert(possessee, client)
543            .expect("Checked entity was alive!");
544
545        // Other components to transfer if they exist.
546        fn transfer_component<C: specs::Component>(
547            storage: &mut specs::WriteStorage<'_, C>,
548            possessor: EcsEntity,
549            possessee: EcsEntity,
550            transform: impl FnOnce(C) -> C,
551        ) {
552            if let Some(c) = storage.remove(possessor) {
553                // Note: we check that the `possessor` and `possessee` entities exist above, so
554                // this should never panic.
555                storage
556                    .insert(possessee, transform(c))
557                    .expect("Checked entity was alive!");
558            }
559        }
560
561        let mut players = ecs.write_storage::<comp::Player>();
562        let mut subscriptions = ecs.write_storage::<RegionSubscription>();
563        let mut admins = ecs.write_storage::<comp::Admin>();
564        let mut waypoints = ecs.write_storage::<comp::Waypoint>();
565        let mut force_updates = ecs.write_storage::<comp::ForceUpdate>();
566
567        transfer_component(&mut players, possessor, possessee, |x| x);
568        transfer_component(&mut subscriptions, possessor, possessee, |x| x);
569        transfer_component(&mut admins, possessor, possessee, |x| x);
570        transfer_component(&mut waypoints, possessor, possessee, |x| x);
571        let mut update_counter = 0;
572        transfer_component(&mut force_updates, possessor, possessee, |mut x| {
573            x.update();
574            update_counter = x.counter();
575            x
576        });
577
578        let mut presences = ecs.write_storage::<Presence>();
579        // We leave Presence on the old entity for character IDs to be properly removed
580        // from the ID mapping if deleting the previous entity.
581        //
582        // If the entity is not going to be deleted, we remove it so that the entity
583        // doesn't keep an area loaded.
584        if delete_entity.is_none() {
585            presences.remove(possessor);
586        }
587        if let Some(p) = new_presence {
588            presences
589                .insert(possessee, p)
590                .expect("Checked entity was alive!");
591        }
592
593        // If a player is possessing, add possessee to playerlist as player and remove
594        // old player.
595        // Fetches from possessee entity here since we have transferred over the
596        // `Player` component.
597        if let Some(player) = players.get(possessee)
598            && emit_player_list_events
599        {
600            use common_net::msg;
601
602            let add_player_msg = ServerGeneral::PlayerListUpdate(PlayerListUpdate::Add(
603                possessee_uid,
604                msg::server::PlayerInfo {
605                    player_alias: player.alias.clone(),
606                    is_online: true,
607                    is_moderator: admins.contains(possessee),
608                    character: ecs.read_storage::<comp::Stats>().get(possessee).map(|s| {
609                        msg::CharacterInfo {
610                            name: s.name.clone(),
611                            // NOTE: hack, read docs on body::Gender for more
612                            gender: s.original_body.humanoid_gender(),
613                            battle_mode: player.battle_mode,
614                        }
615                    }),
616                    uuid: player.uuid(),
617                },
618            ));
619            let remove_player_msg =
620                ServerGeneral::PlayerListUpdate(PlayerListUpdate::Remove(possessor_uid));
621
622            drop((clients, players)); // need to drop so we can use `notify_players` below
623            state.notify_players(remove_player_msg);
624            state.notify_players(add_player_msg);
625        }
626        drop(admins);
627
628        // Put possess item into loadout
629        let time = ecs.read_resource::<Time>();
630        let mut inventories = ecs.write_storage::<Inventory>();
631        let mut inventory = inventories
632            .entry(possessee)
633            .expect("Nobody has &mut World, so there's no way to delete an entity.")
634            .or_insert(Inventory::with_empty());
635
636        let debug_item = comp::Item::new_from_asset_expect("common.items.debug.admin_stick");
637        if let item::ItemKind::Tool(_) = &*debug_item.kind() {
638            let leftover_items = inventory.swap(
639                Slot::Equip(EquipSlot::ActiveMainhand),
640                Slot::Equip(EquipSlot::InactiveMainhand),
641                *time,
642            );
643            assert!(
644                leftover_items.is_empty(),
645                "Swapping active and inactive mainhands never results in leftover items"
646            );
647            inventory.replace_loadout_item(EquipSlot::ActiveMainhand, Some(debug_item), *time);
648        }
649        drop(inventories);
650
651        // Remove will of the entity
652        ecs.write_storage::<comp::Agent>().remove(possessee);
653        // Reset controller of former shell
654        if let Some(c) = ecs.write_storage::<comp::Controller>().get_mut(possessor) {
655            *c = Default::default();
656        }
657
658        // Send client new `SyncFrom::ClientEntity` components and tell it to
659        // deletes these on the old entity.
660        let clients = ecs.read_storage::<Client>();
661        let client = clients
662            .get(possessee)
663            .expect("We insert this component above and have exclusive access to the world.");
664        use crate::sys::sentinel::TrackedStorages;
665        use specs::SystemData;
666        let tracked_storages = TrackedStorages::fetch(ecs);
667        let comp_sync_package = tracked_storages.create_sync_from_client_entity_switch(
668            possessor_uid,
669            possessee_uid,
670            possessee,
671        );
672        if !comp_sync_package.is_empty() {
673            client.send_fallible(ServerGeneral::CompSync(comp_sync_package, update_counter));
674        }
675    }
676
677    // Outside block above to prevent borrow conflicts (i.e. convenient to let
678    // everything drop at the end of the block rather than doing it manually for
679    // this). See note on `persist_entity` call above for why we do this.
680    if let Some(entity) = delete_entity {
681        // Delete old entity
682        if let Err(e) = state.delete_entity_recorded(entity) {
683            error!(
684                ?e,
685                ?entity,
686                "Failed to delete entity when removing character during possession."
687            );
688        }
689    }
690}
691
692pub fn handle_set_battle_mode(
693    server: &mut Server,
694    SetBattleModeEvent {
695        entity,
696        battle_mode,
697    }: SetBattleModeEvent,
698) {
699    server.set_battle_mode_for(entity, battle_mode);
700}