diff --git a/client/src/lib.rs b/client/src/lib.rs index 2dbd2ee85b..4d9c28871c 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1837,12 +1837,35 @@ impl Client { let mut controllers = self.state.ecs().write_storage::(); if let Some(controller) = controllers.remove(old_entity) { if let Err(e) = controllers.insert(entity, controller) { - error!(?e, "Failed to insert controller when setting new player entity!"); + error!( + ?e, + "Failed to insert controller when setting new player entity!" + ); } } + + let uids = self.state.ecs().read_storage::(); + if let Some((prev_uid, presence)) = + uids.get(old_entity).copied().zip(self.presence) + { + self.presence = Some(match presence { + PresenceKind::Character(char_id) => { + PresenceKind::Possessor(char_id, prev_uid) + }, + PresenceKind::Spectator => PresenceKind::Spectator, + PresenceKind::Possessor(old_char_id, old_uid) => { + if old_uid == uid { + // Returning to original entity + PresenceKind::Character(old_char_id) + } else { + PresenceKind::Possessor(old_char_id, old_uid) + } + }, + }); + } } } else { - return Err(Error::Other("Failed to find entity from uid.".to_owned())); + return Err(Error::Other("Failed to find entity from uid.".into())); } }, ServerGeneral::TimeOfDay(time_of_day, calendar) => { diff --git a/common/net/src/msg/mod.rs b/common/net/src/msg/mod.rs index fef7a11cc8..8b082b8257 100644 --- a/common/net/src/msg/mod.rs +++ b/common/net/src/msg/mod.rs @@ -19,13 +19,30 @@ pub use self::{ }, world_msg::WorldMapMsg, }; -use common::character::CharacterId; +use common::{character::CharacterId, uid::Uid}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum PresenceKind { Spectator, Character(CharacterId), + Possessor( + /// The original character Id before possession began. Used to revert + /// back to original `Character` presence if original entity is + /// re-possessed. + CharacterId, + /// The original entity Uid. + Uid, + ), +} + +impl PresenceKind { + /// Check if the presence represents a control of a character, and thus + /// certain in-game messages from the client such as control inputs + /// should be handled. + pub fn controlling_char(&self) -> bool { + matches!(self, Self::Character(_) | Self::Possessor(_, _)) + } } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index a1c6a37404..cda30ed721 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -1,5 +1,4 @@ use specs::{world::WorldExt, Builder, Entity as EcsEntity, Join}; -use tracing::error; use vek::*; use common::{ @@ -9,8 +8,6 @@ use common::{ agent::{AgentEvent, Sound, SoundKind}, dialogue::Subject, inventory::slot::EquipSlot, - item, - slot::Slot, tool::ToolKind, Inventory, Pos, SkillGroupKind, }, @@ -18,19 +15,13 @@ use common::{ link::Is, mounting::{Mount, Mounting, Rider}, outcome::Outcome, - region::RegionMap, terrain::{Block, SpriteKind}, uid::Uid, vol::ReadVol, }; -use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; +use common_net::sync::WorldSyncExt; -use crate::{ - client::Client, - presence::{Presence, RegionSubscription}, - state_ext::StateExt, - Server, -}; +use crate::{state_ext::StateExt, Server}; use crate::pet::tame_pet; use hashbrown::{HashMap, HashSet}; @@ -144,175 +135,6 @@ pub fn handle_unmount(server: &mut Server, rider: EcsEntity) { state.ecs().write_storage::>().remove(rider); } -/// FIXME: This code is dangerous and needs to be refactored. We can't just -/// comment it out, but it needs to be fixed for a variety of reasons. Get rid -/// of this ASAP! -pub fn handle_possess(server: &mut Server, possessor_uid: Uid, possesse_uid: Uid) { - let ecs = server.state.ecs(); - if let (Some(possessor), Some(possesse)) = ( - ecs.entity_from_uid(possessor_uid.into()), - ecs.entity_from_uid(possesse_uid.into()), - ) { - // Check that entities still exist - if !possessor.gen().is_alive() - || !ecs.is_alive(possessor) - || !possesse.gen().is_alive() - || !ecs.is_alive(possesse) - { - error!( - "Error possessing! either the possessor entity or possesse entity no longer exists" - ); - return; - } - - let mut clients = ecs.write_storage::(); - let mut players = ecs.write_storage::(); - - if clients.contains(possesse) || players.contains(possesse) { - error!("Can't possess other players!"); - return; - } - - // Limit possessible entities to those in the client's subscribed regions (so - // that the entity already exists on the client, reduces the amount of - // syncing edge cases to consider). - let mut subscriptions = ecs.write_storage::(); - let region_map = ecs.read_resource::(); - let possesse_in_subscribed_region = subscriptions - .get(possessor) - .iter() - .flat_map(|s| s.regions.iter()) - .filter_map(|key| region_map.get(*key)) - .any(|region| region.entities().contains(possesse.id())); - if !possesse_in_subscribed_region { - return; - } - - // Transfer client component. Note: we require this component for possession. - if let Some(client) = clients.remove(possessor) { - client.send_fallible(ServerGeneral::SetPlayerEntity(possesse_uid)); - // Note: we check that the `possessor` and `possesse` entities exist above, so - // this should never panic. - clients - .insert(possesse, client) - .expect("Checked entity was alive!"); - } else { - error!("Error posessing, no `Client` component on the possessor!"); - return; - }; - - // Other components to transfer if they exist. - // TODO: don't transfer character id, TODO: consider how this could relate to - // database duplications, might need to model this like the player - // logging out. Note: logging back in is delayed because it would - // re-load from the database before old information is saved, if you are - // able to reposess the same entity, this should not be an issue, - // although the logout save being duplicated with the batch save may need to be - // considered, the logout save would be outdated. If you could cause - // your old entity to drop items while possessing another entity that - // would cause duplication in the database (on the other hand this ability - // should be strictly limited to admins, and not intended to be a normal - // gameplay ability). - fn transfer_component( - storage: &mut specs::WriteStorage<'_, C>, - possessor: EcsEntity, - possesse: EcsEntity, - ) { - if let Some(c) = storage.remove(possessor) { - // Note: we check that the `possessor` and `possesse` entities exist above, so - // this should never panic. - storage - .insert(possesse, c) - .expect("Checked entity was alive!"); - } - } - - let mut presence = ecs.write_storage::(); - let mut admins = ecs.write_storage::(); - let mut waypoints = ecs.write_storage::(); - - transfer_component(&mut players, possessor, possesse); - transfer_component(&mut presence, possessor, possesse); - transfer_component(&mut subscriptions, possessor, possesse); - transfer_component(&mut admins, possessor, possesse); - transfer_component(&mut waypoints, possessor, possesse); - - // If a player is posessing, add possesse to playerlist as player and remove old - // player. - // Fetches from possesse entity here since we have transferred over the `Player` - // component. - if let Some(player) = players.get(possesse) { - use common_net::msg; - - let add_player_msg = ServerGeneral::PlayerListUpdate( - msg::server::PlayerListUpdate::Add(possesse_uid, msg::server::PlayerInfo { - player_alias: player.alias.clone(), - is_online: true, - is_moderator: admins.contains(possesse), - character: ecs.read_storage::().get(possesse).map(|s| { - msg::CharacterInfo { - name: s.name.clone(), - } - }), - }), - ); - let remove_player_msg = ServerGeneral::PlayerListUpdate( - msg::server::PlayerListUpdate::Remove(possessor_uid), - ); - - drop((clients, players)); // need to drop so we can use `notify_players` below - server.state().notify_players(remove_player_msg); - server.state().notify_players(add_player_msg); - } - - // Put possess item into loadout - let mut inventories = ecs.write_storage::(); - let mut inventory = inventories - .entry(possesse) - .expect("Nobody has &mut World, so there's no way to delete an entity.") - .or_insert(Inventory::new_empty()); - - let debug_item = comp::Item::new_from_asset_expect("common.items.debug.admin_stick"); - if let item::ItemKind::Tool(_) = debug_item.kind() { - let leftover_items = inventory.swap( - Slot::Equip(EquipSlot::ActiveMainhand), - Slot::Equip(EquipSlot::InactiveMainhand), - ); - assert!( - leftover_items.is_empty(), - "Swapping active and inactive mainhands never results in leftover items" - ); - inventory.replace_loadout_item(EquipSlot::ActiveMainhand, Some(debug_item)); - } - drop(inventories); - - // Remove will of the entity - ecs.write_storage::().remove(possesse); - // Reset controller of former shell - if let Some(c) = ecs.write_storage::().get_mut(possessor) { - *c = Default::default(); - } - - // Send client new `SyncFrom::ClientEntity` components and tell it to - // deletes these on the old entity. - let clients = ecs.read_storage::(); - let client = clients - .get(possesse) - .expect("We insert this component above and have exclusive access to the world."); - use crate::sys::sentinel::TrackedStorages; - use specs::SystemData; - let tracked_storages = TrackedStorages::fetch(ecs); - let comp_sync_package = tracked_storages.create_sync_from_client_entity_switch( - possessor_uid, - possesse_uid, - possesse, - ); - if !comp_sync_package.is_empty() { - client.send_fallible(ServerGeneral::CompSync(comp_sync_package)); - } - } -} - fn within_mounting_range(player_position: Option<&Pos>, mount_position: Option<&Pos>) -> bool { match (player_position, mount_position) { (Some(ppos), Some(ipos)) => ppos.0.distance_squared(ipos.0) < MAX_MOUNT_RANGE.powi(2), diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 6bc384a1f4..bddfea4988 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -18,11 +18,11 @@ use group_manip::handle_group; use information::handle_site_info; use interaction::{ handle_create_sprite, handle_lantern, handle_mine_block, handle_mount, handle_npc_interaction, - handle_possess, handle_sound, handle_unmount, + handle_sound, handle_unmount, }; use inventory_manip::handle_inventory; use invite::{handle_invite, handle_invite_response}; -use player::{handle_client_disconnect, handle_exit_ingame}; +use player::{handle_client_disconnect, handle_exit_ingame, handle_possess}; use specs::{Builder, Entity as EcsEntity, WorldExt}; use trade::{cancel_trade_for, handle_process_trade_action}; diff --git a/server/src/events/player.rs b/server/src/events/player.rs index e06e8300e1..9e72d976b2 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -271,8 +271,222 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity { ); }, PresenceKind::Spectator => { /* Do nothing, spectators do not need persisting */ }, + PresenceKind::Possessor(_, _) => { /* Do nothing, possessor's are not persisted */ }, }; } entity } + +/// FIXME: This code is dangerous and needs to be refactored. We can't just +/// comment it out, but it needs to be fixed for a variety of reasons. Get rid +/// of this ASAP! +pub fn handle_possess(server: &mut Server, possessor_uid: Uid, possesse_uid: Uid) { + use crate::presence::RegionSubscription; + use common::{ + comp::{inventory::slot::EquipSlot, item, slot::Slot, Inventory}, + region::RegionMap, + }; + use common_net::sync::WorldSyncExt; + + let ecs = server.state.ecs(); + + if let (Some(possessor), Some(possesse)) = ( + ecs.entity_from_uid(possessor_uid.into()), + ecs.entity_from_uid(possesse_uid.into()), + ) { + // In this section we check various invariants and can return early if any of + // them are not met. + { + // Check that entities still exist + if !possessor.gen().is_alive() + || !ecs.is_alive(possessor) + || !possesse.gen().is_alive() + || !ecs.is_alive(possesse) + { + error!( + "Error possessing! either the possessor entity or possesse entity no longer \ + exists" + ); + return; + } + + let clients = ecs.read_storage::(); + let players = ecs.read_storage::(); + + if clients.contains(possesse) || players.contains(possesse) { + error!("Can't possess other players!"); + return; + } + + // Limit possessible entities to those in the client's subscribed regions (so + // that the entity already exists on the client, this reduces the + // amount of syncing edge cases to consider). + let subscriptions = ecs.read_storage::(); + let region_map = ecs.read_resource::(); + let possesse_in_subscribed_region = subscriptions + .get(possessor) + .iter() + .flat_map(|s| s.regions.iter()) + .filter_map(|key| region_map.get(*key)) + .any(|region| region.entities().contains(possesse.id())); + if !possesse_in_subscribed_region { + return; + } + + if !clients.contains(possessor) { + error!("Error posessing, no `Client` component on the possessor!"); + return; + } + + // No early returns after this. + } + + // Sync the player's character data to the database. This must be done before + // moving any components from the entity. + drop(ecs); + let state = server.state_mut(); + let possessor = persist_entity(state, possessor); + drop(state); + // TODO: delete old entity (if PresenceKind::Character) as if logging out. + + let ecs = server.state.ecs(); + let mut clients = ecs.write_storage::(); + + // Transfer client component. Note: we require this component for possession. + let client = clients.remove(possessor).expect("Checked client component was present above!"); + client.send_fallible(ServerGeneral::SetPlayerEntity(possesse_uid)); + // Note: we check that the `possessor` and `possesse` entities exist above, so + // this should never panic. + clients.insert(possesse, client).expect("Checked entity was alive!"); + + // Other components to transfer if they exist. + // TODO: don't transfer character id, TODO: consider how this could relate to + // database duplications, might need to model this like the player + // logging out. Note: logging back in is delayed because it would + // re-load from the database before old information is saved, if you are + // able to reposess the same entity, this should not be an issue, + // although the logout save being duplicated with the batch save may need to be + // considered, the logout save would be outdated. If you could cause + // your old entity to drop items while possessing another entity that + // would cause duplication in the database (on the other hand this ability + // should be strictly limited to admins, and not intended to be a normal + // gameplay ability). + fn transfer_component( + storage: &mut specs::WriteStorage<'_, C>, + possessor: EcsEntity, + possesse: EcsEntity, + transform: impl FnOnce(C) -> C, + ) { + if let Some(c) = storage.remove(possessor) { + // Note: we check that the `possessor` and `possesse` entities exist above, so + // this should never panic. + storage + .insert(possesse, transform(c)) + .expect("Checked entity was alive!"); + } + } + + let mut players = ecs.write_storage::(); + let mut presence = ecs.write_storage::(); + let mut subscriptions = ecs.write_storage::(); + let mut admins = ecs.write_storage::(); + let mut waypoints = ecs.write_storage::(); + + transfer_component(&mut players, possessor, possesse, |x| x); + transfer_component(&mut presence, possessor, possesse, |mut presence| { + presence.kind = match presence.kind { + PresenceKind::Spectator => PresenceKind::Spectator, + // TODO: also perform this transition on the client in response to entity switch. + // Disable persistence by changing the presence. + PresenceKind::Character(char_id) => PresenceKind::Possessor(char_id, possessor_uid), + PresenceKind::Possessor(old_char_id, old_uid) => if old_uid == possesse_uid { + // If moving back to the original entity, shift back to the character + // presence which will re-enable persistence. + PresenceKind::Character(old_char_id) + } else { + PresenceKind::Possessor(old_char_id, old_uid) + }, + }; + + presence + }); + transfer_component(&mut subscriptions, possessor, possesse, |x| x); + transfer_component(&mut admins, possessor, possesse, |x| x); + transfer_component(&mut waypoints, possessor, possesse, |x| x); + + // If a player is posessing, add possesse to playerlist as player and remove old + // player. + // Fetches from possesse entity here since we have transferred over the `Player` + // component. + if let Some(player) = players.get(possesse) { + use common_net::msg; + + let add_player_msg = ServerGeneral::PlayerListUpdate( + msg::server::PlayerListUpdate::Add(possesse_uid, msg::server::PlayerInfo { + player_alias: player.alias.clone(), + is_online: true, + is_moderator: admins.contains(possesse), + character: ecs.read_storage::().get(possesse).map(|s| { + msg::CharacterInfo { + name: s.name.clone(), + } + }), + }), + ); + let remove_player_msg = ServerGeneral::PlayerListUpdate( + msg::server::PlayerListUpdate::Remove(possessor_uid), + ); + + drop((clients, players)); // need to drop so we can use `notify_players` below + server.state().notify_players(remove_player_msg); + server.state().notify_players(add_player_msg); + } + + // Put possess item into loadout + let mut inventories = ecs.write_storage::(); + let mut inventory = inventories + .entry(possesse) + .expect("Nobody has &mut World, so there's no way to delete an entity.") + .or_insert(Inventory::new_empty()); + + let debug_item = comp::Item::new_from_asset_expect("common.items.debug.admin_stick"); + if let item::ItemKind::Tool(_) = debug_item.kind() { + let leftover_items = inventory.swap( + Slot::Equip(EquipSlot::ActiveMainhand), + Slot::Equip(EquipSlot::InactiveMainhand), + ); + assert!( + leftover_items.is_empty(), + "Swapping active and inactive mainhands never results in leftover items" + ); + inventory.replace_loadout_item(EquipSlot::ActiveMainhand, Some(debug_item)); + } + drop(inventories); + + // Remove will of the entity + ecs.write_storage::().remove(possesse); + // Reset controller of former shell + if let Some(c) = ecs.write_storage::().get_mut(possessor) { + *c = Default::default(); + } + + // Send client new `SyncFrom::ClientEntity` components and tell it to + // deletes these on the old entity. + let clients = ecs.read_storage::(); + let client = clients + .get(possesse) + .expect("We insert this component above and have exclusive access to the world."); + use crate::sys::sentinel::TrackedStorages; + use specs::SystemData; + let tracked_storages = TrackedStorages::fetch(ecs); + let comp_sync_package = tracked_storages.create_sync_from_client_entity_switch( + possessor_uid, + possesse_uid, + possesse, + ); + if !comp_sync_package.is_empty() { + client.send_fallible(ServerGeneral::CompSync(comp_sync_package)); + } + } +} diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index bef7448b4d..5ee80a324a 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -14,7 +14,7 @@ use common::{ vol::ReadVol, }; use common_ecs::{Job, Origin, Phase, System}; -use common_net::msg::{ClientGeneral, PresenceKind, ServerGeneral}; +use common_net::msg::{ClientGeneral, ServerGeneral}; use common_state::{BlockChange, BuildAreas}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteStorage}; use tracing::{debug, trace, warn}; @@ -84,14 +84,14 @@ impl Sys { } }, ClientGeneral::ControllerInputs(inputs) => { - if matches!(presence.kind, PresenceKind::Character(_)) { + if presence.kind.controlling_char() { if let Some(controller) = controllers.get_mut(entity) { controller.inputs.update_with_new(*inputs); } } }, ClientGeneral::ControlEvent(event) => { - if matches!(presence.kind, PresenceKind::Character(_)) { + if presence.kind.controlling_char() { // Skip respawn if client entity is alive if let ControlEvent::Respawn = event { if healths.get(entity).map_or(true, |h| !h.is_dead) { @@ -105,7 +105,7 @@ impl Sys { } }, ClientGeneral::ControlAction(event) => { - if matches!(presence.kind, PresenceKind::Character(_)) { + if presence.kind.controlling_char() { if let Some(controller) = controllers.get_mut(entity) { controller.push_action(event); } @@ -119,7 +119,7 @@ impl Sys { .or_default() }); - if matches!(presence.kind, PresenceKind::Character(_)) + if presence.kind.controlling_char() && force_updates.get(entity).is_none() && healths.get(entity).map_or(true, |h| !h.is_dead) && is_rider.get(entity).is_none() diff --git a/server/src/sys/persistence.rs b/server/src/sys/persistence.rs index 270520147f..e39c1768a2 100644 --- a/server/src/sys/persistence.rs +++ b/server/src/sys/persistence.rs @@ -100,7 +100,7 @@ impl<'a> System<'a> for Sys { map_marker, )) }, - PresenceKind::Spectator => None, + PresenceKind::Spectator | PresenceKind::Possessor(_, _) => None, }, ), ); diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 1152f065e5..d731645a38 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -1022,8 +1022,9 @@ impl Hud { // point. let character_id = match client.presence().unwrap() { - PresenceKind::Character(id) => id, + PresenceKind::Character(id) => Some(id), PresenceKind::Spectator => unreachable!("HUD creation in Spectator mode!"), + PresenceKind::Possessor(_, _) => None, }; // Create a new HotbarState from the persisted slots. diff --git a/voxygen/src/lib.rs b/voxygen/src/lib.rs index 2c6c8d2196..bd54371f29 100644 --- a/voxygen/src/lib.rs +++ b/voxygen/src/lib.rs @@ -8,7 +8,8 @@ bool_to_option, drain_filter, once_cell, - trait_alias + trait_alias, + option_get_or_insert_default )] #![recursion_limit = "2048"] diff --git a/voxygen/src/profile.rs b/voxygen/src/profile.rs index 1006e04c99..83e6164300 100644 --- a/voxygen/src/profile.rs +++ b/voxygen/src/profile.rs @@ -35,7 +35,7 @@ impl Default for CharacterProfile { pub struct ServerProfile { /// A map of character's by id to their CharacterProfile. pub characters: HashMap, - // Selected character in the chararacter selection screen + /// Selected character in the chararacter selection screen pub selected_character: Option, } @@ -53,18 +53,14 @@ impl Default for ServerProfile { /// Initially it is just for persisting things that don't belong in /// settings.ron - like the state of hotbar and any other character level /// configuration. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct Profile { pub servers: HashMap, -} - -impl Default for Profile { - fn default() -> Self { - Profile { - servers: HashMap::new(), - } - } + /// Temporary character profiler, used when it should + /// not be persisted to the disk. + #[serde(skip)] + pub transient_character: Option, } impl Profile { @@ -112,17 +108,22 @@ impl Profile { /// # Arguments /// /// * server - current server the character is on. - /// * character_id - id of the character. + /// * character_id - id of the character, passing `None` indicates the + /// transient character profile should be used. pub fn get_hotbar_slots( &self, server: &str, - character_id: CharacterId, + character_id: Option, ) -> [Option; 10] { - self.servers - .get(server) - .and_then(|s| s.characters.get(&character_id)) - .map(|c| c.hotbar_slots.clone()) - .unwrap_or_else(default_slots) + match character_id { + Some(character_id) => self + .servers + .get(server) + .and_then(|s| s.characters.get(&character_id)), + None => self.transient_character.as_ref(), + } + .map(|c| c.hotbar_slots.clone()) + .unwrap_or_else(default_slots) } /// Set the hotbar_slots for the requested character_id. @@ -133,22 +134,26 @@ impl Profile { /// # Arguments /// /// * server - current server the character is on. - /// * character_id - id of the character. + /// * character_id - id of the character, passing `None` indicates the + /// transient character profile should be used. /// * slots - array of hotbar_slots to save. pub fn set_hotbar_slots( &mut self, server: &str, - character_id: CharacterId, + character_id: Option, slots: [Option; 10], ) { - self.servers - .entry(server.to_string()) - .or_insert(ServerProfile::default()) - // Get or update the CharacterProfile. - .characters - .entry(character_id) - .or_insert(CharacterProfile::default()) - .hotbar_slots = slots; + match character_id { + Some(character_id) => self.servers + .entry(server.to_string()) + .or_insert(ServerProfile::default()) + // Get or update the CharacterProfile. + .characters + .entry(character_id) + .or_default(), + None => self.transient_character.get_or_insert_default(), + } + .hotbar_slots = slots; } /// Get the selected_character for the provided server. diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index ccbd8f93e4..70a133c23d 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -1361,10 +1361,11 @@ impl PlayState for SessionState { let server_name = &client.server_info().name; // If we are changing the hotbar state this CANNOT be None. let character_id = match client.presence().unwrap() { - PresenceKind::Character(id) => id, + PresenceKind::Character(id) => Some(id), PresenceKind::Spectator => { unreachable!("HUD adaption in Spectator mode!") }, + PresenceKind::Possessor(_, _) => None, }; // Get or update the ServerProfile.