From b9a3fa1edc5bba15589760b26291215637e51171 Mon Sep 17 00:00:00 2001 From: crabman Date: Sun, 11 Feb 2024 12:11:04 +0100 Subject: [PATCH 1/4] transform server event --- common/src/event.rs | 4 + server/src/cmd.rs | 74 +++----------- server/src/events/entity_manipulation.rs | 121 ++++++++++++++++++++++- server/src/events/mod.rs | 14 ++- server/src/events/player.rs | 4 +- server/src/pet.rs | 2 +- server/src/state_ext.rs | 4 +- 7 files changed, 148 insertions(+), 75 deletions(-) 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)! From 036e79284e235e94ecf439744d7c761b7d0a7446 Mon Sep 17 00:00:00 2001 From: crabman Date: Sun, 11 Feb 2024 13:46:32 +0100 Subject: [PATCH 2/4] transform character state --- .../common/abilities/ability_set_manifest.ron | 1 + assets/common/abilities/debug/evolve.ron | 6 + assets/voxygen/i18n/en/hud/ability.ftl | 2 + common/src/comp/ability.rs | 49 ++++++- common/src/comp/character_state.rs | 14 ++ common/src/states/mod.rs | 1 + common/src/states/transform.rs | 125 ++++++++++++++++++ common/systems/src/stats.rs | 1 + voxygen/src/scene/particle.rs | 32 +++++ 9 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 assets/common/abilities/debug/evolve.ron create mode 100644 common/src/states/transform.rs diff --git a/assets/common/abilities/ability_set_manifest.ron b/assets/common/abilities/ability_set_manifest.ron index a142621547..ec7fd52fd4 100644 --- a/assets/common/abilities/ability_set_manifest.ron +++ b/assets/common/abilities/ability_set_manifest.ron @@ -930,6 +930,7 @@ secondary: Simple(None, "common.abilities.debug.upboost"), abilities: [ Simple(None, "common.abilities.debug.possess"), + Simple(None, "common.abilities.debug.evolve"), ], ), Tool(Farming): ( diff --git a/assets/common/abilities/debug/evolve.ron b/assets/common/abilities/debug/evolve.ron new file mode 100644 index 0000000000..128f53cf4c --- /dev/null +++ b/assets/common/abilities/debug/evolve.ron @@ -0,0 +1,6 @@ +Transform( + buildup_duration: 2.0, + recover_duration: 0.5, + target: "common.entity.wild.peaceful.crab", + specifier: Some(Evolve), +) diff --git a/assets/voxygen/i18n/en/hud/ability.ftl b/assets/voxygen/i18n/en/hud/ability.ftl index b46856e1d9..d2d23a745f 100644 --- a/assets/voxygen/i18n/en/hud/ability.ftl +++ b/assets/voxygen/i18n/en/hud/ability.ftl @@ -1,5 +1,7 @@ common-abilities-debug-possess = Possessing Arrow .desc = Shoots a poisonous arrow. Lets you control your target. +common-abilities-debug-evolve = Evolve + .desc = You become your better self. common-abilities-hammer-leap = Smash of Doom .desc = An AOE attack with knockback. Leaps to position of cursor. common-abilities-bow-shotgun = Burst diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 46167a7d5c..f1ae20c54b 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -646,6 +646,7 @@ impl From<&CharacterState> for CharacterAbilityType { | CharacterState::UseItem(_) | CharacterState::SpriteInteract(_) | CharacterState::Skate(_) + | CharacterState::Transform(_) | CharacterState::Wallrun(_) => Self::Other, } } @@ -997,6 +998,15 @@ pub enum CharacterAbility { #[serde(default)] meta: AbilityMeta, }, + Transform { + buildup_duration: f32, + recover_duration: f32, + target: String, + #[serde(default)] + specifier: Option, + #[serde(default)] + meta: AbilityMeta, + }, } impl Default for CharacterAbility { @@ -1115,7 +1125,8 @@ impl CharacterAbility { | CharacterAbility::Blink { .. } | CharacterAbility::Music { .. } | CharacterAbility::BasicSummon { .. } - | CharacterAbility::SpriteSummon { .. } => true, + | CharacterAbility::SpriteSummon { .. } + | CharacterAbility::Transform { .. } => true, } } @@ -1662,6 +1673,16 @@ impl CharacterAbility { *energy_cost /= stats.energy_efficiency; *melee_constructor = melee_constructor.adjusted_by_stats(stats); }, + Transform { + ref mut buildup_duration, + ref mut recover_duration, + target: _, + specifier: _, + meta: _, + } => { + *buildup_duration /= stats.speed; + *recover_duration /= stats.speed; + }, } self } @@ -1702,7 +1723,8 @@ impl CharacterAbility { | Blink { .. } | Music { .. } | BasicSummon { .. } - | SpriteSummon { .. } => 0.0, + | SpriteSummon { .. } + | Transform { .. } => 0.0, } } @@ -1750,7 +1772,8 @@ impl CharacterAbility { | Blink { .. } | Music { .. } | BasicSummon { .. } - | SpriteSummon { .. } => 0, + | SpriteSummon { .. } + | Transform { .. } => 0, } } @@ -1782,7 +1805,8 @@ impl CharacterAbility { | Music { meta, .. } | DiveMelee { meta, .. } | RiposteMelee { meta, .. } - | RapidMelee { meta, .. } => *meta, + | RapidMelee { meta, .. } + | Transform { meta, .. } => *meta, } } @@ -2935,6 +2959,23 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { stage_section: StageSection::Buildup, exhausted: false, }), + CharacterAbility::Transform { + buildup_duration, + recover_duration, + target, + specifier, + meta: _, + } => CharacterState::Transform(transform::Data { + static_data: transform::StaticData { + buildup_duration: Duration::from_secs_f32(*buildup_duration), + recover_duration: Duration::from_secs_f32(*recover_duration), + specifier: *specifier, + target: target.to_owned(), + ability_info, + }, + timer: Duration::default(), + stage_section: StageSection::Buildup, + }), } } } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 9aa88a8251..a60bb68f71 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -51,6 +51,7 @@ event_emitters! { energy_change: event::EnergyChangeEvent, knockback: event::KnockbackEvent, sprite_light: event::ToggleSpriteLightEvent, + transform: event::TransformEvent, } } @@ -172,6 +173,8 @@ pub enum CharacterState { /// A series of consecutive, identical attacks that only go through buildup /// and recover once for the entire state RapidMelee(rapid_melee::Data), + /// Transforms an entity into another + Transform(transform::Data), } impl CharacterState { @@ -518,6 +521,7 @@ impl CharacterState { CharacterState::DiveMelee(data) => data.behavior(j, output_events), CharacterState::RiposteMelee(data) => data.behavior(j, output_events), CharacterState::RapidMelee(data) => data.behavior(j, output_events), + CharacterState::Transform(data) => data.behavior(j, output_events), } } @@ -573,6 +577,7 @@ impl CharacterState { CharacterState::DiveMelee(data) => data.handle_event(j, output_events, action), CharacterState::RiposteMelee(data) => data.handle_event(j, output_events, action), CharacterState::RapidMelee(data) => data.handle_event(j, output_events, action), + CharacterState::Transform(data) => data.handle_event(j, output_events, action), } } @@ -625,6 +630,7 @@ impl CharacterState { CharacterState::DiveMelee(data) => Some(data.static_data.ability_info), CharacterState::RiposteMelee(data) => Some(data.static_data.ability_info), CharacterState::RapidMelee(data) => Some(data.static_data.ability_info), + CharacterState::Transform(data) => Some(data.static_data.ability_info), } } @@ -669,6 +675,7 @@ impl CharacterState { CharacterState::DiveMelee(data) => Some(data.stage_section), CharacterState::RiposteMelee(data) => Some(data.stage_section), CharacterState::RapidMelee(data) => Some(data.stage_section), + CharacterState::Transform(data) => Some(data.stage_section), } } @@ -857,6 +864,11 @@ impl CharacterState { recover: Some(data.static_data.recover_duration), ..Default::default() }), + CharacterState::Transform(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), } } @@ -901,6 +913,7 @@ impl CharacterState { CharacterState::DiveMelee(data) => Some(data.timer), CharacterState::RiposteMelee(data) => Some(data.timer), CharacterState::RapidMelee(data) => Some(data.timer), + CharacterState::Transform(data) => Some(data.timer), } } @@ -960,6 +973,7 @@ impl CharacterState { CharacterState::DiveMelee(_) => Some(AttackSource::Melee), CharacterState::RiposteMelee(_) => Some(AttackSource::Melee), CharacterState::RapidMelee(_) => Some(AttackSource::Melee), + CharacterState::Transform(_) => None, } } } diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 31d96c0625..5ad0cdd153 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -35,6 +35,7 @@ pub mod sprite_interact; pub mod sprite_summon; pub mod stunned; pub mod talk; +pub mod transform; pub mod use_item; pub mod utils; pub mod wallrun; diff --git a/common/src/states/transform.rs b/common/src/states/transform.rs new file mode 100644 index 0000000000..b8a98cbfc3 --- /dev/null +++ b/common/src/states/transform.rs @@ -0,0 +1,125 @@ +use std::time::Duration; + +use common_assets::AssetExt; +use rand::thread_rng; +use serde::{Deserialize, Serialize}; +use vek::Vec3; + +use crate::{ + comp::{item::Reagent, CharacterState, StateUpdate}, + event::TransformEvent, + generation::{EntityConfig, EntityInfo}, + states::utils::{end_ability, tick_attack_or_default}, +}; + +use super::{ + behavior::CharacterBehavior, + utils::{AbilityInfo, StageSection}, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub enum FrontendSpecifier { + Evolve, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct StaticData { + /// How long until state has until transformation + pub buildup_duration: Duration, + /// How long the state has until exiting + pub recover_duration: Duration, + /// The entity configuration you will be transformed into + pub target: String, + pub ability_info: AbilityInfo, + /// Used to specify the transformation to the frontend + pub specifier: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Data { + /// Struct containing data that does not change over the course of the + /// character state + pub static_data: StaticData, + /// Timer for each stage + pub timer: Duration, + /// What section the character stage is in + pub stage_section: StageSection, +} + +impl CharacterBehavior for Data { + fn behavior( + &self, + data: &super::behavior::JoinData, + output_events: &mut crate::comp::character_state::OutputEvents, + ) -> crate::comp::StateUpdate { + let mut update = StateUpdate::from(data); + match self.stage_section { + StageSection::Buildup => { + // Tick the timer as long as buildup hasn't finihsed + if self.timer < self.static_data.buildup_duration { + update.character = CharacterState::Transform(Data { + static_data: self.static_data.clone(), + timer: tick_attack_or_default(data, self.timer, None), + ..*self + }); + // Buildup finished, start transformation + } else { + let Ok(entity_config) = EntityConfig::load(&self.static_data.target) else { + end_ability(data, &mut update); + return update; + }; + + let entity_info = EntityInfo::at(Vec3::zero()).with_entity_config( + entity_config.read().clone(), + Some(&self.static_data.target), + &mut thread_rng(), + None, + ); + + // Handle frontend events + if let Some(specifier) = self.static_data.specifier { + match specifier { + FrontendSpecifier::Evolve => { + output_events.emit_local(crate::event::LocalEvent::CreateOutcome( + crate::outcome::Outcome::Explosion { + pos: data.pos.0, + power: 5.0, + radius: 2.0, + is_attack: false, + reagent: Some(Reagent::White), + }, + )) + }, + } + } + + output_events.emit_server(TransformEvent(*data.uid, entity_info)); + update.character = CharacterState::Transform(Data { + static_data: self.static_data.clone(), + timer: Duration::default(), + stage_section: StageSection::Recover, + }); + } + }, + StageSection::Recover => { + // Wait for recovery period to finish + if self.timer < self.static_data.recover_duration { + update.character = CharacterState::Transform(Data { + static_data: self.static_data.clone(), + timer: tick_attack_or_default(data, self.timer, None), + ..*self + }); + } else { + // End the ability after recovery is done + end_ability(data, &mut update); + } + }, + _ => { + // If we somehow ended up in an incorrect character state, end the ability + end_ability(data, &mut update); + }, + } + + update + } +} diff --git a/common/systems/src/stats.rs b/common/systems/src/stats.rs index 2758e38f8e..830e234d55 100644 --- a/common/systems/src/stats.rs +++ b/common/systems/src/stats.rs @@ -209,6 +209,7 @@ impl<'a> System<'a> for Sys { | CharacterState::Stunned(_) | CharacterState::BasicBlock(_) | CharacterState::UseItem(_) + | CharacterState::Transform(_) | CharacterState::SpriteInteract(_) => {}, } }); diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index f24f7b76f9..333d22b923 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -1384,6 +1384,38 @@ impl ParticleMgr { }); } }, + CharacterState::Transform(data) => { + if matches!(data.stage_section, StageSection::Buildup) + && let Some(specifier) = data.static_data.specifier + { + match specifier { + states::transform::FrontendSpecifier::Evolve => { + self.particles.resize_with( + self.particles.len() + + usize::from( + self.scheduler.heartbeats(Duration::from_millis(10)), + ), + || { + let start_pos = interpolated.pos + + (Vec2::unit_y() + * rng.gen::() + * body.max_radius()) + .rotated_z(rng.gen_range(0.0..(PI * 2.0))) + .with_z(body.height() * rng.gen::()); + + Particle::new_directed( + Duration::from_millis(100), + time, + ParticleMode::BarrelOrgan, + start_pos, + start_pos + Vec3::unit_z() * 2.0, + ) + }, + ) + }, + } + } + }, _ => {}, } } From a14c2f054ca8e3b3a03f224f0695cdab4c5fc948 Mon Sep 17 00:00:00 2001 From: crabman Date: Tue, 27 Feb 2024 08:37:40 +0000 Subject: [PATCH 3/4] addressed review comments --- assets/voxygen/i18n/en/command.ftl | 1 + common/src/states/transform.rs | 2 + server/src/cmd.rs | 7 +-- server/src/events/entity_manipulation.rs | 56 +++++++++++++++++++----- server/src/events/mod.rs | 1 + server/src/events/player.rs | 13 +++--- 6 files changed, 60 insertions(+), 20 deletions(-) diff --git a/assets/voxygen/i18n/en/command.ftl b/assets/voxygen/i18n/en/command.ftl index 4b76ba0b6a..824382c4f7 100644 --- a/assets/voxygen/i18n/en/command.ftl +++ b/assets/voxygen/i18n/en/command.ftl @@ -84,6 +84,7 @@ command-repaired-items = Repaired all equipped items command-message-group-missing = You are using group chat but do not belong to a group. Use /world or /region to change chat. command-tell-request = { $sender } wants to talk to you. +command-transform-invalid-presence = Cannot transform in the current presence # Unreachable/untestable but added for consistency diff --git a/common/src/states/transform.rs b/common/src/states/transform.rs index b8a98cbfc3..cebcce5ae2 100644 --- a/common/src/states/transform.rs +++ b/common/src/states/transform.rs @@ -3,6 +3,7 @@ use std::time::Duration; use common_assets::AssetExt; use rand::thread_rng; use serde::{Deserialize, Serialize}; +use tracing::error; use vek::Vec3; use crate::{ @@ -65,6 +66,7 @@ impl CharacterBehavior for Data { // Buildup finished, start transformation } else { let Ok(entity_config) = EntityConfig::load(&self.static_data.target) else { + error!(?self.static_data.target, "Failed to load entity configuration"); end_ability(data, &mut update); return update; }; diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 55eebf444f..02a10350b4 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -672,9 +672,10 @@ fn handle_into_npc( TransformEntityError::UnexpectedNpcTeleporter => { Content::localized("command-unimplemented-teleporter-spawn") }, - })?; - - Ok(()) + TransformEntityError::LoadingCharacter => { + Content::localized("command-transform-invalid-presence") + }, + }) } fn handle_make_npc( diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index ec753a66a3..d7827ad60e 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -22,7 +22,7 @@ use common::{ item::flatten_counted_items, loot_owner::LootOwnerKind, Alignment, Auras, Body, CharacterState, Energy, Group, Health, Inventory, Object, Player, - Poise, Pos, Presence, PresenceKind, SkillSet, Stats, + Poise, Pos, Presence, PresenceKind, SkillSet, Stats, BASE_ABILITY_LIMIT, }, consts::TELEPORTER_RADIUS, event::{ @@ -2106,13 +2106,17 @@ pub fn handle_transform(server: &mut Server, TransformEvent(uid, info): Transfor return; }; - let _ = transform_entity(server, entity, info); + if let Err(error) = transform_entity(server, entity, info) { + error!(?error, ?uid, "Failed transform entity"); + } } +#[derive(Debug)] pub enum TransformEntityError { EntityDead, UnexpectedNpcWaypoint, UnexpectedNpcTeleporter, + LoadingCharacter, } pub fn transform_entity( @@ -2158,20 +2162,38 @@ pub fn transform_entity( } // Disable persistence - { + 'persist: { + match server + .state + .ecs() + .read_storage::() + .get(entity) + .map(|presence| presence.kind) + { + // Transforming while the character is being loaded or is spectating is invalid! + Some(PresenceKind::Spectator | PresenceKind::LoadingCharacter(_)) => { + return Err(TransformEntityError::LoadingCharacter); + }, + Some(PresenceKind::Possessor | PresenceKind::Character(_)) => {}, + None => break 'persist, + } + // Run persistence once before disabling it + // + // We must NOT early return between persist_entity() being called and + // persistence being set to Possessor 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); + // We re-fetch presence here as mutable, because checking for a valid + // [`PresenceKind`] must be done BEFORE persist_entity but persist_entity needs + // exclusive mutable access to the server's state + let mut presences = server.state.ecs().write_storage::(); + let Some(presence) = presences.get_mut(entity) else { + // Checked above + unreachable!("We already know this entity has a Presence"); + }; - if let Some(presence) = presence - && let PresenceKind::Character(id) = presence.kind - { + if let PresenceKind::Character(id) = presence.kind { server.state.ecs().write_resource::().remove_entity( Some(entity), None, @@ -2194,6 +2216,16 @@ pub fn transform_entity( 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))?; + // Reset active abilities + set_or_remove_component( + server, + entity, + Some(if body.is_humanoid() { + comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT) + } else { + comp::ActiveAbilities::default() + }), + )?; // Don't add Agent or ItemDrops to players if !is_player { diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 9967ab680f..6bcb708f68 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -37,6 +37,7 @@ mod mounting; mod player; mod trade; +/// Shared utilities used by other code **in this crate** pub(crate) mod shared { pub(crate) use super::{ entity_manipulation::{transform_entity, TransformEntityError}, diff --git a/server/src/events/player.rs b/server/src/events/player.rs index ab210bdef0..db8fcd1b9b 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -243,11 +243,14 @@ pub fn handle_client_disconnect( Event::ClientDisconnected { entity } } -// When a player logs out, their data is queued for persistence in the next tick -// of the persistence batch update. The player will be -// 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. +/// When a player logs out, their data is queued for persistence in the next +/// tick of the persistence batch update. The player will be +/// 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. +/// +/// This function is also used by the Transform event and MUST NOT assume that +/// the persisting entity is deleted afterwards. pub(super) fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity { if let ( Some(presence), From c50f1047f78cb54143d32bae90cc213b2dc38acc Mon Sep 17 00:00:00 2001 From: crabman Date: Wed, 28 Feb 2024 15:56:12 +0000 Subject: [PATCH 4/4] Added allow_players flag to Transform character state --- assets/common/abilities/debug/evolve.ron | 1 + common/src/comp/ability.rs | 7 +++++++ common/src/event.rs | 8 +++++++- common/src/states/transform.rs | 8 +++++++- server/src/cmd.rs | 7 ++++++- server/src/events/entity_manipulation.rs | 24 ++++++++++++++++++------ 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/assets/common/abilities/debug/evolve.ron b/assets/common/abilities/debug/evolve.ron index 128f53cf4c..f9e1a71fd7 100644 --- a/assets/common/abilities/debug/evolve.ron +++ b/assets/common/abilities/debug/evolve.ron @@ -3,4 +3,5 @@ Transform( recover_duration: 0.5, target: "common.entity.wild.peaceful.crab", specifier: Some(Evolve), + allow_players: true, ) diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index f1ae20c54b..2e26ed2d06 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -1004,6 +1004,10 @@ pub enum CharacterAbility { target: String, #[serde(default)] specifier: Option, + /// Only set to `true` for admin only abilities since this disables + /// persistence and is not intended to be used by regular players + #[serde(default)] + allow_players: bool, #[serde(default)] meta: AbilityMeta, }, @@ -1678,6 +1682,7 @@ impl CharacterAbility { ref mut recover_duration, target: _, specifier: _, + allow_players: _, meta: _, } => { *buildup_duration /= stats.speed; @@ -2964,12 +2969,14 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { recover_duration, target, specifier, + allow_players, meta: _, } => CharacterState::Transform(transform::Data { static_data: transform::StaticData { buildup_duration: Duration::from_secs_f32(*buildup_duration), recover_duration: Duration::from_secs_f32(*recover_duration), specifier: *specifier, + allow_players: *allow_players, target: target.to_owned(), ability_info, }, diff --git a/common/src/event.rs b/common/src/event.rs index b189956129..2869fb02b0 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -261,7 +261,13 @@ 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 TransformEvent { + pub target_entity: Uid, + pub entity_info: EntityInfo, + /// If set to false, players wont be transformed unless with a Possessor + /// presence kind + pub allow_players: bool, +} pub struct InitializeCharacterEvent { pub entity: EcsEntity, diff --git a/common/src/states/transform.rs b/common/src/states/transform.rs index cebcce5ae2..83c67d07ef 100644 --- a/common/src/states/transform.rs +++ b/common/src/states/transform.rs @@ -32,6 +32,8 @@ pub struct StaticData { /// The entity configuration you will be transformed into pub target: String, pub ability_info: AbilityInfo, + /// Whether players are allowed to transform + pub allow_players: bool, /// Used to specify the transformation to the frontend pub specifier: Option, } @@ -95,7 +97,11 @@ impl CharacterBehavior for Data { } } - output_events.emit_server(TransformEvent(*data.uid, entity_info)); + output_events.emit_server(TransformEvent { + target_entity: *data.uid, + entity_info, + allow_players: self.static_data.allow_players, + }); update.character = CharacterState::Transform(Data { static_data: self.static_data.clone(), timer: Duration::default(), diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 02a10350b4..895b683b02 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -662,7 +662,7 @@ fn handle_into_npc( None, ); - transform_entity(server, target, entity_info).map_err(|error| match error { + transform_entity(server, target, entity_info, true).map_err(|error| match error { TransformEntityError::EntityDead => { Content::localized_with_args("command-entity-dead", [("entity", "target")]) }, @@ -675,6 +675,11 @@ fn handle_into_npc( TransformEntityError::LoadingCharacter => { Content::localized("command-transform-invalid-presence") }, + TransformEntityError::EntityIsPlayer => { + unreachable!( + "Transforming players must be valid as we explicitly allowed player transformation" + ); + }, }) } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index d7827ad60e..90928450fc 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -2101,13 +2101,20 @@ 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 { +pub fn handle_transform( + server: &mut Server, + TransformEvent { + target_entity, + entity_info, + allow_players, + }: TransformEvent, +) { + let Some(entity) = server.state().ecs().entity_from_uid(target_entity) else { return; }; - if let Err(error) = transform_entity(server, entity, info) { - error!(?error, ?uid, "Failed transform entity"); + if let Err(error) = transform_entity(server, entity, entity_info, allow_players) { + error!(?error, ?target_entity, "Failed transform entity"); } } @@ -2117,19 +2124,21 @@ pub enum TransformEntityError { UnexpectedNpcWaypoint, UnexpectedNpcTeleporter, LoadingCharacter, + EntityIsPlayer, } pub fn transform_entity( server: &mut Server, entity: Entity, - info: EntityInfo, + entity_info: EntityInfo, + allow_players: bool, ) -> Result<(), TransformEntityError> { let is_player = server .state() .read_storage::() .contains(entity); - match NpcData::from_entity_info(info) { + match NpcData::from_entity_info(entity_info) { NpcData::Data { inventory, stats, @@ -2174,6 +2183,9 @@ pub fn transform_entity( Some(PresenceKind::Spectator | PresenceKind::LoadingCharacter(_)) => { return Err(TransformEntityError::LoadingCharacter); }, + Some(PresenceKind::Character(_)) if !allow_players => { + return Err(TransformEntityError::EntityIsPlayer); + }, Some(PresenceKind::Possessor | PresenceKind::Character(_)) => {}, None => break 'persist, }