Merge branch 'crabman/transform' into 'master'

Transform server event & character state

See merge request veloren/veloren!4320
This commit is contained in:
crabman 2024-02-29 00:22:19 +00:00
commit 74978bf166
17 changed files with 459 additions and 86 deletions

View File

@ -930,6 +930,7 @@
secondary: Simple(None, "common.abilities.debug.upboost"), secondary: Simple(None, "common.abilities.debug.upboost"),
abilities: [ abilities: [
Simple(None, "common.abilities.debug.possess"), Simple(None, "common.abilities.debug.possess"),
Simple(None, "common.abilities.debug.evolve"),
], ],
), ),
Tool(Farming): ( Tool(Farming): (

View File

@ -0,0 +1,7 @@
Transform(
buildup_duration: 2.0,
recover_duration: 0.5,
target: "common.entity.wild.peaceful.crab",
specifier: Some(Evolve),
allow_players: true,
)

View File

@ -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 command-message-group-missing = You are using group chat but do not belong to a group. Use /world or
/region to change chat. /region to change chat.
command-tell-request = { $sender } wants to talk to you. 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 # Unreachable/untestable but added for consistency

View File

@ -1,5 +1,7 @@
common-abilities-debug-possess = Possessing Arrow common-abilities-debug-possess = Possessing Arrow
.desc = Shoots a poisonous arrow. Lets you control your target. .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 common-abilities-hammer-leap = Smash of Doom
.desc = An AOE attack with knockback. Leaps to position of cursor. .desc = An AOE attack with knockback. Leaps to position of cursor.
common-abilities-bow-shotgun = Burst common-abilities-bow-shotgun = Burst

View File

@ -646,6 +646,7 @@ impl From<&CharacterState> for CharacterAbilityType {
| CharacterState::UseItem(_) | CharacterState::UseItem(_)
| CharacterState::SpriteInteract(_) | CharacterState::SpriteInteract(_)
| CharacterState::Skate(_) | CharacterState::Skate(_)
| CharacterState::Transform(_)
| CharacterState::Wallrun(_) => Self::Other, | CharacterState::Wallrun(_) => Self::Other,
} }
} }
@ -997,6 +998,19 @@ pub enum CharacterAbility {
#[serde(default)] #[serde(default)]
meta: AbilityMeta, meta: AbilityMeta,
}, },
Transform {
buildup_duration: f32,
recover_duration: f32,
target: String,
#[serde(default)]
specifier: Option<transform::FrontendSpecifier>,
/// 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,
},
} }
impl Default for CharacterAbility { impl Default for CharacterAbility {
@ -1115,7 +1129,8 @@ impl CharacterAbility {
| CharacterAbility::Blink { .. } | CharacterAbility::Blink { .. }
| CharacterAbility::Music { .. } | CharacterAbility::Music { .. }
| CharacterAbility::BasicSummon { .. } | CharacterAbility::BasicSummon { .. }
| CharacterAbility::SpriteSummon { .. } => true, | CharacterAbility::SpriteSummon { .. }
| CharacterAbility::Transform { .. } => true,
} }
} }
@ -1662,6 +1677,17 @@ impl CharacterAbility {
*energy_cost /= stats.energy_efficiency; *energy_cost /= stats.energy_efficiency;
*melee_constructor = melee_constructor.adjusted_by_stats(stats); *melee_constructor = melee_constructor.adjusted_by_stats(stats);
}, },
Transform {
ref mut buildup_duration,
ref mut recover_duration,
target: _,
specifier: _,
allow_players: _,
meta: _,
} => {
*buildup_duration /= stats.speed;
*recover_duration /= stats.speed;
},
} }
self self
} }
@ -1702,7 +1728,8 @@ impl CharacterAbility {
| Blink { .. } | Blink { .. }
| Music { .. } | Music { .. }
| BasicSummon { .. } | BasicSummon { .. }
| SpriteSummon { .. } => 0.0, | SpriteSummon { .. }
| Transform { .. } => 0.0,
} }
} }
@ -1750,7 +1777,8 @@ impl CharacterAbility {
| Blink { .. } | Blink { .. }
| Music { .. } | Music { .. }
| BasicSummon { .. } | BasicSummon { .. }
| SpriteSummon { .. } => 0, | SpriteSummon { .. }
| Transform { .. } => 0,
} }
} }
@ -1782,7 +1810,8 @@ impl CharacterAbility {
| Music { meta, .. } | Music { meta, .. }
| DiveMelee { meta, .. } | DiveMelee { meta, .. }
| RiposteMelee { meta, .. } | RiposteMelee { meta, .. }
| RapidMelee { meta, .. } => *meta, | RapidMelee { meta, .. }
| Transform { meta, .. } => *meta,
} }
} }
@ -2935,6 +2964,25 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
stage_section: StageSection::Buildup, stage_section: StageSection::Buildup,
exhausted: false, exhausted: false,
}), }),
CharacterAbility::Transform {
buildup_duration,
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,
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
}),
} }
} }
} }

View File

@ -51,6 +51,7 @@ event_emitters! {
energy_change: event::EnergyChangeEvent, energy_change: event::EnergyChangeEvent,
knockback: event::KnockbackEvent, knockback: event::KnockbackEvent,
sprite_light: event::ToggleSpriteLightEvent, 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 /// A series of consecutive, identical attacks that only go through buildup
/// and recover once for the entire state /// and recover once for the entire state
RapidMelee(rapid_melee::Data), RapidMelee(rapid_melee::Data),
/// Transforms an entity into another
Transform(transform::Data),
} }
impl CharacterState { impl CharacterState {
@ -518,6 +521,7 @@ impl CharacterState {
CharacterState::DiveMelee(data) => data.behavior(j, output_events), CharacterState::DiveMelee(data) => data.behavior(j, output_events),
CharacterState::RiposteMelee(data) => data.behavior(j, output_events), CharacterState::RiposteMelee(data) => data.behavior(j, output_events),
CharacterState::RapidMelee(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::DiveMelee(data) => data.handle_event(j, output_events, action),
CharacterState::RiposteMelee(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::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::DiveMelee(data) => Some(data.static_data.ability_info),
CharacterState::RiposteMelee(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::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::DiveMelee(data) => Some(data.stage_section),
CharacterState::RiposteMelee(data) => Some(data.stage_section), CharacterState::RiposteMelee(data) => Some(data.stage_section),
CharacterState::RapidMelee(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), recover: Some(data.static_data.recover_duration),
..Default::default() ..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::DiveMelee(data) => Some(data.timer),
CharacterState::RiposteMelee(data) => Some(data.timer), CharacterState::RiposteMelee(data) => Some(data.timer),
CharacterState::RapidMelee(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::DiveMelee(_) => Some(AttackSource::Melee),
CharacterState::RiposteMelee(_) => Some(AttackSource::Melee), CharacterState::RiposteMelee(_) => Some(AttackSource::Melee),
CharacterState::RapidMelee(_) => Some(AttackSource::Melee), CharacterState::RapidMelee(_) => Some(AttackSource::Melee),
CharacterState::Transform(_) => None,
} }
} }
} }

View File

@ -9,6 +9,7 @@ use crate::{
misc::PortalData, misc::PortalData,
DisconnectReason, LootOwner, Ori, Pos, UnresolvedChatMsg, Vel, DisconnectReason, LootOwner, Ori, Pos, UnresolvedChatMsg, Vel,
}, },
generation::EntityInfo,
lottery::LootSpec, lottery::LootSpec,
mounting::VolumePos, mounting::VolumePos,
outcome::Outcome, outcome::Outcome,
@ -260,6 +261,14 @@ pub struct SetPetStayEvent(pub EcsEntity, pub EcsEntity, pub bool);
pub struct PossessEvent(pub Uid, pub Uid); pub struct PossessEvent(pub Uid, pub Uid);
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 struct InitializeCharacterEvent {
pub entity: EcsEntity, pub entity: EcsEntity,
pub character_id: CharacterId, pub character_id: CharacterId,
@ -531,6 +540,7 @@ pub fn register_event_busses(ecs: &mut World) {
ecs.insert(EventBus::<TeleportToPositionEvent>::default()); ecs.insert(EventBus::<TeleportToPositionEvent>::default());
ecs.insert(EventBus::<StartTeleportingEvent>::default()); ecs.insert(EventBus::<StartTeleportingEvent>::default());
ecs.insert(EventBus::<ToggleSpriteLightEvent>::default()); ecs.insert(EventBus::<ToggleSpriteLightEvent>::default());
ecs.insert(EventBus::<TransformEvent>::default());
} }
/// Define ecs read data for event busses. And a way to convert them all to /// Define ecs read data for event busses. And a way to convert them all to

View File

@ -35,6 +35,7 @@ pub mod sprite_interact;
pub mod sprite_summon; pub mod sprite_summon;
pub mod stunned; pub mod stunned;
pub mod talk; pub mod talk;
pub mod transform;
pub mod use_item; pub mod use_item;
pub mod utils; pub mod utils;
pub mod wallrun; pub mod wallrun;

View File

@ -0,0 +1,133 @@
use std::time::Duration;
use common_assets::AssetExt;
use rand::thread_rng;
use serde::{Deserialize, Serialize};
use tracing::error;
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,
/// Whether players are allowed to transform
pub allow_players: bool,
/// Used to specify the transformation to the frontend
pub specifier: Option<FrontendSpecifier>,
}
#[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 {
error!(?self.static_data.target, "Failed to load entity configuration");
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 {
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(),
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
}
}

View File

@ -209,6 +209,7 @@ impl<'a> System<'a> for Sys {
| CharacterState::Stunned(_) | CharacterState::Stunned(_)
| CharacterState::BasicBlock(_) | CharacterState::BasicBlock(_)
| CharacterState::UseItem(_) | CharacterState::UseItem(_)
| CharacterState::Transform(_)
| CharacterState::SpriteInteract(_) => {}, | CharacterState::SpriteInteract(_) => {},
} }
}); });

View File

@ -35,8 +35,7 @@ use common::{
}, },
invite::InviteKind, invite::InviteKind,
misc::PortalData, misc::PortalData,
AdminRole, ChatType, Content, Inventory, Item, LightEmitter, Presence, PresenceKind, AdminRole, ChatType, Content, Inventory, Item, LightEmitter, WaypointArea,
WaypointArea,
}, },
depot, depot,
effect::Effect, effect::Effect,
@ -54,7 +53,7 @@ use common::{
rtsim::{Actor, Role}, rtsim::{Actor, Role},
terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize}, terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize},
tether::Tethered, tether::Tethered,
uid::{IdMaps, Uid}, uid::Uid,
vol::ReadVol, vol::ReadVol,
weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect,
}; };
@ -628,6 +627,8 @@ fn handle_into_npc(
args: Vec<String>, args: Vec<String>,
action: &ServerChatCommand, action: &ServerChatCommand,
) -> CmdResult<()> { ) -> CmdResult<()> {
use crate::events::shared::{transform_entity, TransformEntityError};
if client != target { if client != target {
server.notify_client( server.notify_client(
client, client,
@ -661,70 +662,25 @@ fn handle_into_npc(
None, None,
); );
match NpcData::from_entity_info(entity_info) { transform_entity(server, target, entity_info, true).map_err(|error| match error {
NpcData::Data { TransformEntityError::EntityDead => {
inventory, Content::localized_with_args("command-entity-dead", [("entity", "target")])
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")?;
}, },
NpcData::Waypoint(_) => { TransformEntityError::UnexpectedNpcWaypoint => {
return Err(Content::localized("command-unimplemented-waypoint-spawn")); Content::localized("command-unimplemented-waypoint-spawn")
}, },
NpcData::Teleporter(_, _) => { TransformEntityError::UnexpectedNpcTeleporter => {
return Err(Content::localized("command-unimplemented-teleporter-spawn")); Content::localized("command-unimplemented-teleporter-spawn")
}, },
} TransformEntityError::LoadingCharacter => {
Content::localized("command-transform-invalid-presence")
// Black magic },
// TransformEntityError::EntityIsPlayer => {
// Mainly needed to disable persistence unreachable!(
{ "Transforming players must be valid as we explicitly allowed player transformation"
// 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::<Presence>();
let presence = presences.get_mut(target);
if let Some(presence) = presence
&& let PresenceKind::Character(id) = presence.kind
{
server.state.ecs().write_resource::<IdMaps>().remove_entity(
Some(target),
None,
Some(id),
None,
); );
},
presence.kind = PresenceKind::Possessor; })
}
}
// End of black magic
Ok(())
} }
fn handle_make_npc( fn handle_make_npc(

View File

@ -9,7 +9,7 @@ use crate::{
error, error,
rtsim::RtSim, rtsim::RtSim,
state_ext::StateExt, state_ext::StateExt,
sys::terrain::SAFE_ZONE_RADIUS, sys::terrain::{NpcData, SAFE_ZONE_RADIUS},
Server, Settings, SpawnPoint, Server, Settings, SpawnPoint,
}; };
use common::{ use common::{
@ -22,7 +22,7 @@ use common::{
item::flatten_counted_items, item::flatten_counted_items,
loot_owner::LootOwnerKind, loot_owner::LootOwnerKind,
Alignment, Auras, Body, CharacterState, Energy, Group, Health, Inventory, Object, Player, 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, consts::TELEPORTER_RADIUS,
event::{ event::{
@ -31,9 +31,11 @@ use common::{
DestroyEvent, EmitExt, Emitter, EnergyChangeEvent, EntityAttackedHookEvent, EventBus, DestroyEvent, EmitExt, Emitter, EnergyChangeEvent, EntityAttackedHookEvent, EventBus,
ExplosionEvent, HealthChangeEvent, KnockbackEvent, LandOnGroundEvent, MakeAdminEvent, ExplosionEvent, HealthChangeEvent, KnockbackEvent, LandOnGroundEvent, MakeAdminEvent,
ParryHookEvent, PoiseChangeEvent, RemoveLightEmitterEvent, RespawnEvent, SoundEvent, ParryHookEvent, PoiseChangeEvent, RemoveLightEmitterEvent, RespawnEvent, SoundEvent,
StartTeleportingEvent, TeleportToEvent, TeleportToPositionEvent, UpdateMapMarkerEvent, StartTeleportingEvent, TeleportToEvent, TeleportToPositionEvent, TransformEvent,
UpdateMapMarkerEvent,
}, },
event_emitters, event_emitters,
generation::EntityInfo,
link::Is, link::Is,
lottery::distribute_many, lottery::distribute_many,
mounting::{Rider, VolumeRider}, mounting::{Rider, VolumeRider},
@ -49,13 +51,13 @@ use common::{
vol::ReadVol, vol::ReadVol,
CachedSpatialGrid, Damage, DamageKind, DamageSource, GroupTarget, RadiusEffect, 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 common_state::{AreasContainer, BlockChange, NoDurabilityArea};
use hashbrown::HashSet; use hashbrown::HashSet;
use rand::Rng; use rand::Rng;
use specs::{ use specs::{
shred, DispatcherBuilder, Entities, Entity as EcsEntity, Entity, Join, LendJoin, Read, 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 std::{collections::HashMap, iter, sync::Arc, time::Duration};
use tracing::{debug, warn}; use tracing::{debug, warn};
@ -2098,3 +2100,158 @@ impl ServerEvent for StartTeleportingEvent {
} }
} }
} }
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, entity_info, allow_players) {
error!(?error, ?target_entity, "Failed transform entity");
}
}
#[derive(Debug)]
pub enum TransformEntityError {
EntityDead,
UnexpectedNpcWaypoint,
UnexpectedNpcTeleporter,
LoadingCharacter,
EntityIsPlayer,
}
pub fn transform_entity(
server: &mut Server,
entity: Entity,
entity_info: EntityInfo,
allow_players: bool,
) -> Result<(), TransformEntityError> {
let is_player = server
.state()
.read_storage::<comp::Player>()
.contains(entity);
match NpcData::from_entity_info(entity_info) {
NpcData::Data {
inventory,
stats,
skill_set,
poise,
health,
body,
scale,
agent,
loot,
alignment: _,
pos: _,
} => {
fn set_or_remove_component<C: specs::Component>(
server: &mut Server,
entity: EcsEntity,
component: Option<C>,
) -> Result<(), TransformEntityError> {
let mut storage = server.state.ecs_mut().write_storage::<C>();
if let Some(component) = component {
storage
.insert(entity, component)
.and(Ok(()))
.map_err(|_| TransformEntityError::EntityDead)
} else {
storage.remove(entity);
Ok(())
}
}
// Disable persistence
'persist: {
match server
.state
.ecs()
.read_storage::<Presence>()
.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::Character(_)) if !allow_players => {
return Err(TransformEntityError::EntityIsPlayer);
},
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);
// 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::<Presence>();
let Some(presence) = presences.get_mut(entity) else {
// Checked above
unreachable!("We already know this entity has a Presence");
};
if let PresenceKind::Character(id) = presence.kind {
server.state.ecs().write_resource::<IdMaps>().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))?;
// 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 {
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(())
}

View File

@ -11,16 +11,13 @@ use specs::{
WriteExpect, WriteExpect,
}; };
pub use group_manip::update_map_markers;
pub(crate) use trade::cancel_trades_for;
use self::{ use self::{
entity_creation::{ entity_creation::{
handle_create_item_drop, handle_create_npc, handle_create_object, handle_create_ship, handle_create_item_drop, handle_create_npc, handle_create_object, handle_create_ship,
handle_create_teleporter, handle_create_waypoint, handle_initialize_character, handle_create_teleporter, handle_create_waypoint, handle_initialize_character,
handle_initialize_spectator, handle_loaded_character_data, handle_shockwave, handle_shoot, 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, interaction::handle_tame_pet,
mounting::{handle_mount, handle_mount_volume, handle_unmount}, mounting::{handle_mount, handle_mount_volume, handle_unmount},
player::{ player::{
@ -40,6 +37,15 @@ mod mounting;
mod player; mod player;
mod trade; mod trade;
/// Shared utilities used by other code **in this crate**
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 { pub trait ServerEvent: Send + Sync + 'static {
type SystemData<'a>: specs::SystemData<'a>; type SystemData<'a>: specs::SystemData<'a>;
@ -165,6 +171,7 @@ impl Server {
)); ));
}); });
self.handle_serial_events(handle_possess); self.handle_serial_events(handle_possess);
self.handle_serial_events(handle_transform);
self.handle_serial_events(|this, ev: CommandEvent| { self.handle_serial_events(|this, ev: CommandEvent| {
this.process_command(ev.0, ev.1, ev.2); this.process_command(ev.0, ev.1, ev.2);
}); });

View File

@ -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 // Cancel trades here since we don't use `delete_entity_recorded` and we
// remove `Uid` below. // remove `Uid` below.
super::cancel_trades_for(state, entity); super::trade::cancel_trades_for(state, entity);
let maybe_group = state.read_component_copied::<group::Group>(entity); let maybe_group = state.read_component_copied::<group::Group>(entity);
let maybe_admin = state.delete_component::<comp::Admin>(entity); let maybe_admin = state.delete_component::<comp::Admin>(entity);
@ -243,12 +243,15 @@ pub fn handle_client_disconnect(
Event::ClientDisconnected { entity } Event::ClientDisconnected { entity }
} }
// When a player logs out, their data is queued for persistence in the next tick /// When a player logs out, their data is queued for persistence in the next
// of the persistence batch update. The player will be /// tick of the persistence batch update. The player will be
// temporarily unable to log in during this period to avoid /// temporarily unable to log in during this period to avoid
// the race condition of their login fetching their old data /// the race condition of their login fetching their old data
// and overwriting the data saved here. /// and overwriting the data saved here.
fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity { ///
/// 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 ( if let (
Some(presence), Some(presence),
Some(skill_set), Some(skill_set),

View File

@ -1,4 +1,4 @@
use crate::{client::Client, events::update_map_markers}; use crate::{client::Client, events::shared::update_map_markers};
use common::{ use common::{
comp::{ comp::{
self, anchor::Anchor, group::GroupManager, Agent, Alignment, Behavior, BehaviorCapability, self, anchor::Anchor, group::GroupManager, Agent, Alignment, Behavior, BehaviorCapability,

View File

@ -2,7 +2,7 @@ use crate::{
automod::AutoMod, automod::AutoMod,
chat::ChatExporter, chat::ChatExporter,
client::Client, client::Client,
events::{self, update_map_markers}, events::{self, shared::update_map_markers},
persistence::PersistedComponents, persistence::PersistedComponents,
pet::restore_pet, pet::restore_pet,
presence::RepositionOnChunkLoad, presence::RepositionOnChunkLoad,
@ -1212,7 +1212,7 @@ impl StateExt for State {
} }
// Cancel extant trades // 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 // NOTE: We expect that these 3 components are never removed from an entity (nor
// mutated) (at least not without updating the relevant mappings)! // mutated) (at least not without updating the relevant mappings)!

View File

@ -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::<f32>()
* body.max_radius())
.rotated_z(rng.gen_range(0.0..(PI * 2.0)))
.with_z(body.height() * rng.gen::<f32>());
Particle::new_directed(
Duration::from_millis(100),
time,
ParticleMode::BarrelOrgan,
start_pos,
start_pos + Vec3::unit_z() * 2.0,
)
},
)
},
}
}
},
_ => {}, _ => {},
} }
} }