mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'crabman/transform' into 'master'
Transform server event & character state See merge request veloren/veloren!4320
This commit is contained in:
commit
74978bf166
@ -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): (
|
||||
|
7
assets/common/abilities/debug/evolve.ron
Normal file
7
assets/common/abilities/debug/evolve.ron
Normal 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,
|
||||
)
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -646,6 +646,7 @@ impl From<&CharacterState> for CharacterAbilityType {
|
||||
| CharacterState::UseItem(_)
|
||||
| CharacterState::SpriteInteract(_)
|
||||
| CharacterState::Skate(_)
|
||||
| CharacterState::Transform(_)
|
||||
| CharacterState::Wallrun(_) => Self::Other,
|
||||
}
|
||||
}
|
||||
@ -997,6 +998,19 @@ pub enum CharacterAbility {
|
||||
#[serde(default)]
|
||||
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 {
|
||||
@ -1115,7 +1129,8 @@ impl CharacterAbility {
|
||||
| CharacterAbility::Blink { .. }
|
||||
| CharacterAbility::Music { .. }
|
||||
| CharacterAbility::BasicSummon { .. }
|
||||
| CharacterAbility::SpriteSummon { .. } => true,
|
||||
| CharacterAbility::SpriteSummon { .. }
|
||||
| CharacterAbility::Transform { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1662,6 +1677,17 @@ 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: _,
|
||||
allow_players: _,
|
||||
meta: _,
|
||||
} => {
|
||||
*buildup_duration /= stats.speed;
|
||||
*recover_duration /= stats.speed;
|
||||
},
|
||||
}
|
||||
self
|
||||
}
|
||||
@ -1702,7 +1728,8 @@ impl CharacterAbility {
|
||||
| Blink { .. }
|
||||
| Music { .. }
|
||||
| BasicSummon { .. }
|
||||
| SpriteSummon { .. } => 0.0,
|
||||
| SpriteSummon { .. }
|
||||
| Transform { .. } => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1750,7 +1777,8 @@ impl CharacterAbility {
|
||||
| Blink { .. }
|
||||
| Music { .. }
|
||||
| BasicSummon { .. }
|
||||
| SpriteSummon { .. } => 0,
|
||||
| SpriteSummon { .. }
|
||||
| Transform { .. } => 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1782,7 +1810,8 @@ impl CharacterAbility {
|
||||
| Music { meta, .. }
|
||||
| DiveMelee { 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,
|
||||
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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,14 @@ pub struct SetPetStayEvent(pub EcsEntity, pub EcsEntity, pub bool);
|
||||
|
||||
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 entity: EcsEntity,
|
||||
pub character_id: CharacterId,
|
||||
@ -531,6 +540,7 @@ pub fn register_event_busses(ecs: &mut World) {
|
||||
ecs.insert(EventBus::<TeleportToPositionEvent>::default());
|
||||
ecs.insert(EventBus::<StartTeleportingEvent>::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
|
||||
|
@ -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;
|
||||
|
133
common/src/states/transform.rs
Normal file
133
common/src/states/transform.rs
Normal 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
|
||||
}
|
||||
}
|
@ -209,6 +209,7 @@ impl<'a> System<'a> for Sys {
|
||||
| CharacterState::Stunned(_)
|
||||
| CharacterState::BasicBlock(_)
|
||||
| CharacterState::UseItem(_)
|
||||
| CharacterState::Transform(_)
|
||||
| CharacterState::SpriteInteract(_) => {},
|
||||
}
|
||||
});
|
||||
|
@ -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<String>,
|
||||
action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
use crate::events::shared::{transform_entity, TransformEntityError};
|
||||
|
||||
if client != target {
|
||||
server.notify_client(
|
||||
client,
|
||||
@ -661,70 +662,25 @@ 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, true).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::<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,
|
||||
TransformEntityError::LoadingCharacter => {
|
||||
Content::localized("command-transform-invalid-presence")
|
||||
},
|
||||
TransformEntityError::EntityIsPlayer => {
|
||||
unreachable!(
|
||||
"Transforming players must be valid as we explicitly allowed player transformation"
|
||||
);
|
||||
|
||||
presence.kind = PresenceKind::Possessor;
|
||||
}
|
||||
}
|
||||
// End of black magic
|
||||
Ok(())
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_make_npc(
|
||||
|
@ -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::{
|
||||
@ -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::{
|
||||
@ -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,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(())
|
||||
}
|
||||
|
@ -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,15 @@ 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},
|
||||
group_manip::update_map_markers,
|
||||
trade::cancel_trades_for,
|
||||
};
|
||||
}
|
||||
|
||||
pub trait ServerEvent: Send + Sync + 'static {
|
||||
type SystemData<'a>: specs::SystemData<'a>;
|
||||
|
||||
@ -165,6 +171,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);
|
||||
});
|
||||
|
@ -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::<group::Group>(entity);
|
||||
let maybe_admin = state.delete_component::<comp::Admin>(entity);
|
||||
@ -243,12 +243,15 @@ 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.
|
||||
fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
|
||||
/// 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),
|
||||
Some(skill_set),
|
||||
|
@ -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,
|
||||
|
@ -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)!
|
||||
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user