diff --git a/common/src/event.rs b/common/src/event.rs index ac897d885f..b189956129 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -9,6 +9,7 @@ use crate::{ misc::PortalData, DisconnectReason, LootOwner, Ori, Pos, UnresolvedChatMsg, Vel, }, + generation::EntityInfo, lottery::LootSpec, mounting::VolumePos, outcome::Outcome, @@ -260,6 +261,8 @@ pub struct SetPetStayEvent(pub EcsEntity, pub EcsEntity, pub bool); pub struct PossessEvent(pub Uid, pub Uid); +pub struct TransformEvent(pub Uid, pub EntityInfo); + pub struct InitializeCharacterEvent { pub entity: EcsEntity, pub character_id: CharacterId, @@ -531,6 +534,7 @@ pub fn register_event_busses(ecs: &mut World) { ecs.insert(EventBus::::default()); ecs.insert(EventBus::::default()); ecs.insert(EventBus::::default()); + ecs.insert(EventBus::::default()); } /// Define ecs read data for event busses. And a way to convert them all to diff --git a/server/src/cmd.rs b/server/src/cmd.rs index efe48ae1d2..55eebf444f 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -35,8 +35,7 @@ use common::{ }, invite::InviteKind, misc::PortalData, - AdminRole, ChatType, Content, Inventory, Item, LightEmitter, Presence, PresenceKind, - WaypointArea, + AdminRole, ChatType, Content, Inventory, Item, LightEmitter, WaypointArea, }, depot, effect::Effect, @@ -54,7 +53,7 @@ use common::{ rtsim::{Actor, Role}, terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize}, tether::Tethered, - uid::{IdMaps, Uid}, + uid::Uid, vol::ReadVol, weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, }; @@ -628,6 +627,8 @@ fn handle_into_npc( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { + use crate::events::shared::{transform_entity, TransformEntityError}; + if client != target { server.notify_client( client, @@ -661,69 +662,18 @@ fn handle_into_npc( None, ); - match NpcData::from_entity_info(entity_info) { - NpcData::Data { - inventory, - stats, - skill_set, - poise, - health, - body, - scale, - // changing alignments is cool idea, but needs more work - alignment: _, - // we aren't interested in these (yet?) - pos: _, - agent: _, - loot: _, - } => { - // Should do basically what StateExt::create_npc does - insert_or_replace_component(server, target, inventory, "player")?; - insert_or_replace_component(server, target, stats, "player")?; - insert_or_replace_component(server, target, skill_set, "player")?; - insert_or_replace_component(server, target, poise, "player")?; - if let Some(health) = health { - insert_or_replace_component(server, target, health, "player")?; - } - insert_or_replace_component(server, target, body, "player")?; - insert_or_replace_component(server, target, body.mass(), "player")?; - insert_or_replace_component(server, target, body.density(), "player")?; - insert_or_replace_component(server, target, body.collider(), "player")?; - insert_or_replace_component(server, target, scale, "player")?; + transform_entity(server, target, entity_info).map_err(|error| match error { + TransformEntityError::EntityDead => { + Content::localized_with_args("command-entity-dead", [("entity", "target")]) }, - NpcData::Waypoint(_) => { - return Err(Content::localized("command-unimplemented-waypoint-spawn")); + TransformEntityError::UnexpectedNpcWaypoint => { + Content::localized("command-unimplemented-waypoint-spawn") }, - NpcData::Teleporter(_, _) => { - return Err(Content::localized("command-unimplemented-teleporter-spawn")); + TransformEntityError::UnexpectedNpcTeleporter => { + Content::localized("command-unimplemented-teleporter-spawn") }, - } + })?; - // Black magic - // - // Mainly needed to disable persistence - { - // TODO: let Imbris work out some edge-cases: - // - error on PresenseKind::LoadingCharacter - // - handle active inventory actions - let ecs = server.state.ecs(); - let mut presences = ecs.write_storage::(); - let presence = presences.get_mut(target); - - if let Some(presence) = presence - && let PresenceKind::Character(id) = presence.kind - { - server.state.ecs().write_resource::().remove_entity( - Some(target), - None, - Some(id), - None, - ); - - presence.kind = PresenceKind::Possessor; - } - } - // End of black magic Ok(()) } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 1581a857c0..ec753a66a3 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -9,7 +9,7 @@ use crate::{ error, rtsim::RtSim, state_ext::StateExt, - sys::terrain::SAFE_ZONE_RADIUS, + sys::terrain::{NpcData, SAFE_ZONE_RADIUS}, Server, Settings, SpawnPoint, }; use common::{ @@ -31,9 +31,11 @@ use common::{ DestroyEvent, EmitExt, Emitter, EnergyChangeEvent, EntityAttackedHookEvent, EventBus, ExplosionEvent, HealthChangeEvent, KnockbackEvent, LandOnGroundEvent, MakeAdminEvent, ParryHookEvent, PoiseChangeEvent, RemoveLightEmitterEvent, RespawnEvent, SoundEvent, - StartTeleportingEvent, TeleportToEvent, TeleportToPositionEvent, UpdateMapMarkerEvent, + StartTeleportingEvent, TeleportToEvent, TeleportToPositionEvent, TransformEvent, + UpdateMapMarkerEvent, }, event_emitters, + generation::EntityInfo, link::Is, lottery::distribute_many, mounting::{Rider, VolumeRider}, @@ -49,13 +51,13 @@ use common::{ vol::ReadVol, CachedSpatialGrid, Damage, DamageKind, DamageSource, GroupTarget, RadiusEffect, }; -use common_net::msg::ServerGeneral; +use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_state::{AreasContainer, BlockChange, NoDurabilityArea}; use hashbrown::HashSet; use rand::Rng; use specs::{ shred, DispatcherBuilder, Entities, Entity as EcsEntity, Entity, Join, LendJoin, Read, - ReadExpect, ReadStorage, SystemData, Write, WriteExpect, WriteStorage, + ReadExpect, ReadStorage, SystemData, WorldExt, Write, WriteExpect, WriteStorage, }; use std::{collections::HashMap, iter, sync::Arc, time::Duration}; use tracing::{debug, warn}; @@ -2098,3 +2100,114 @@ impl ServerEvent for StartTeleportingEvent { } } } + +pub fn handle_transform(server: &mut Server, TransformEvent(uid, info): TransformEvent) { + let Some(entity) = server.state().ecs().entity_from_uid(uid) else { + return; + }; + + let _ = transform_entity(server, entity, info); +} + +pub enum TransformEntityError { + EntityDead, + UnexpectedNpcWaypoint, + UnexpectedNpcTeleporter, +} + +pub fn transform_entity( + server: &mut Server, + entity: Entity, + info: EntityInfo, +) -> Result<(), TransformEntityError> { + let is_player = server + .state() + .read_storage::() + .contains(entity); + + match NpcData::from_entity_info(info) { + NpcData::Data { + inventory, + stats, + skill_set, + poise, + health, + body, + scale, + agent, + loot, + alignment: _, + pos: _, + } => { + fn set_or_remove_component( + server: &mut Server, + entity: EcsEntity, + component: Option, + ) -> Result<(), TransformEntityError> { + let mut storage = server.state.ecs_mut().write_storage::(); + + if let Some(component) = component { + storage + .insert(entity, component) + .and(Ok(())) + .map_err(|_| TransformEntityError::EntityDead) + } else { + storage.remove(entity); + Ok(()) + } + } + + // Disable persistence + { + // Run persistence once before disabling it + super::player::persist_entity(server.state_mut(), entity); + + // TODO: let Imbris work out some edge-cases: + // - error on PresenseKind::LoadingCharacter + // - handle active inventory actions + let ecs = server.state.ecs(); + let mut presences = ecs.write_storage::(); + let presence = presences.get_mut(entity); + + if let Some(presence) = presence + && let PresenceKind::Character(id) = presence.kind + { + server.state.ecs().write_resource::().remove_entity( + Some(entity), + None, + Some(id), + None, + ); + + presence.kind = PresenceKind::Possessor; + } + } + + // Should do basically what StateExt::create_npc does + set_or_remove_component(server, entity, Some(inventory))?; + set_or_remove_component(server, entity, Some(stats))?; + set_or_remove_component(server, entity, Some(skill_set))?; + set_or_remove_component(server, entity, Some(poise))?; + set_or_remove_component(server, entity, health)?; + set_or_remove_component(server, entity, Some(body))?; + set_or_remove_component(server, entity, Some(body.mass()))?; + set_or_remove_component(server, entity, Some(body.density()))?; + set_or_remove_component(server, entity, Some(body.collider()))?; + set_or_remove_component(server, entity, Some(scale))?; + + // Don't add Agent or ItemDrops to players + if !is_player { + set_or_remove_component(server, entity, agent)?; + set_or_remove_component(server, entity, loot.to_items().map(comp::ItemDrops))?; + } + }, + NpcData::Waypoint(_) => { + return Err(TransformEntityError::UnexpectedNpcWaypoint); + }, + NpcData::Teleporter(_, _) => { + return Err(TransformEntityError::UnexpectedNpcTeleporter); + }, + } + + Ok(()) +} diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index c19d32bd21..9967ab680f 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -11,16 +11,13 @@ use specs::{ WriteExpect, }; -pub use group_manip::update_map_markers; -pub(crate) use trade::cancel_trades_for; - use self::{ entity_creation::{ handle_create_item_drop, handle_create_npc, handle_create_object, handle_create_ship, handle_create_teleporter, handle_create_waypoint, handle_initialize_character, handle_initialize_spectator, handle_loaded_character_data, handle_shockwave, handle_shoot, }, - entity_manipulation::handle_delete, + entity_manipulation::{handle_delete, handle_transform}, interaction::handle_tame_pet, mounting::{handle_mount, handle_mount_volume, handle_unmount}, player::{ @@ -40,6 +37,14 @@ mod mounting; mod player; mod trade; +pub(crate) mod shared { + pub(crate) use super::{ + entity_manipulation::{transform_entity, TransformEntityError}, + group_manip::update_map_markers, + trade::cancel_trades_for, + }; +} + pub trait ServerEvent: Send + Sync + 'static { type SystemData<'a>: specs::SystemData<'a>; @@ -165,6 +170,7 @@ impl Server { )); }); self.handle_serial_events(handle_possess); + self.handle_serial_events(handle_transform); self.handle_serial_events(|this, ev: CommandEvent| { this.process_command(ev.0, ev.1, ev.2); }); diff --git a/server/src/events/player.rs b/server/src/events/player.rs index aed2788fc8..ab210bdef0 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -65,7 +65,7 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity, skip_persisten // Cancel trades here since we don't use `delete_entity_recorded` and we // remove `Uid` below. - super::cancel_trades_for(state, entity); + super::trade::cancel_trades_for(state, entity); let maybe_group = state.read_component_copied::(entity); let maybe_admin = state.delete_component::(entity); @@ -248,7 +248,7 @@ pub fn handle_client_disconnect( // temporarily unable to log in during this period to avoid // the race condition of their login fetching their old data // and overwriting the data saved here. -fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity { +pub(super) fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity { if let ( Some(presence), Some(skill_set), diff --git a/server/src/pet.rs b/server/src/pet.rs index bfc184d07c..d8db6424c3 100644 --- a/server/src/pet.rs +++ b/server/src/pet.rs @@ -1,4 +1,4 @@ -use crate::{client::Client, events::update_map_markers}; +use crate::{client::Client, events::shared::update_map_markers}; use common::{ comp::{ self, anchor::Anchor, group::GroupManager, Agent, Alignment, Behavior, BehaviorCapability, diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 405bd2e2e8..cc298868cb 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -2,7 +2,7 @@ use crate::{ automod::AutoMod, chat::ChatExporter, client::Client, - events::{self, update_map_markers}, + events::{self, shared::update_map_markers}, persistence::PersistedComponents, pet::restore_pet, presence::RepositionOnChunkLoad, @@ -1212,7 +1212,7 @@ impl StateExt for State { } // Cancel extant trades - events::cancel_trades_for(self, entity); + events::shared::cancel_trades_for(self, entity); // NOTE: We expect that these 3 components are never removed from an entity (nor // mutated) (at least not without updating the relevant mappings)!