diff --git a/crates/valence_entity/build.rs b/crates/valence_entity/build.rs index 1e98a1c79..f8aa347b5 100644 --- a/crates/valence_entity/build.rs +++ b/crates/valence_entity/build.rs @@ -279,10 +279,12 @@ type Entities = BTreeMap; pub fn main() -> anyhow::Result<()> { rerun_if_changed(["extracted/misc.json", "extracted/entities.json"]); - write_generated_file(build()?, "entity.rs") + write_generated_file(build_entities()?, "entity.rs")?; + + Ok(()) } -fn build() -> anyhow::Result { +fn build_entities() -> anyhow::Result { let entity_types = serde_json::from_str::(include_str!("extracted/misc.json")) .context("failed to deserialize misc.json")? .entity_type; @@ -399,13 +401,20 @@ fn build() -> anyhow::Result { bundle_init_fields.extend([quote! { living_attributes: super::attributes::EntityAttributes::new() #attribute_default_values, }]); + bundle_fields.extend([quote! { pub living_attributes_tracker: super::attributes::TrackedEntityAttributes, }]); - bundle_init_fields.extend([quote! { living_attributes_tracker: Default::default(), }]); + + bundle_fields.extend([quote! { + pub living_active_status_effects: super::active_status_effects::ActiveStatusEffects, + }]); + bundle_init_fields.extend([quote! { + living_active_status_effects: Default::default(), + }]); } "PlayerEntity" => { bundle_fields.extend([quote! { diff --git a/crates/valence_entity/extracted/entities.json b/crates/valence_entity/extracted/entities.json index 0d2c99faf..f7638662d 100644 --- a/crates/valence_entity/extracted/entities.json +++ b/crates/valence_entity/extracted/entities.json @@ -4768,9 +4768,9 @@ } ], "default_bounding_box": { - "size_x": 0.699999988079071, - "size_y": 0.5, - "size_z": 0.699999988079071 + "size_x": 1.399999976158142, + "size_y": 0.8999999761581421, + "size_z": 1.399999976158142 } }, "SquidEntity": { @@ -6105,7 +6105,7 @@ "type": "villager_data", "default_value": { "type": "plains", - "profession": "nitwit", + "profession": "none", "level": 1 } } diff --git a/crates/valence_entity/src/active_status_effects.rs b/crates/valence_entity/src/active_status_effects.rs new file mode 100644 index 000000000..5ab1492ae --- /dev/null +++ b/crates/valence_entity/src/active_status_effects.rs @@ -0,0 +1,525 @@ +use bevy_ecs::prelude::*; +use indexmap::IndexMap; +use valence_protocol::status_effects::StatusEffect; + +/// Represents a change in the [`ActiveStatusEffects`] of an [`Entity`]. +#[derive(Debug)] +enum StatusEffectChange { + Apply(ActiveStatusEffect), + Replace(ActiveStatusEffect), + Remove(StatusEffect), + RemoveAll, + /// **For internal use only.** + #[doc(hidden)] + Expire(StatusEffect), +} + +/// The result of a duration calculation for a status effect. +pub enum DurationResult { + /// There are no effects of the given type. + NoEffects, + /// The effect has an infinite duration. + Infinite, + /// The effect has a finite duration, represented as an integer number of + /// ticks. + Finite(i32), +} + +/// [`Component`] that stores the [`ActiveStatusEffect`]s of an [`Entity`]. +#[derive(Component, Default, Debug)] +pub struct ActiveStatusEffects { + /// vec is always sorted in descending order of amplifier and ascending + /// order of duration. + current_effects: IndexMap>, + changes: Vec, +} + +// public API +impl ActiveStatusEffects { + /// Applies a new [`ActiveStatusEffect`]. + /// + /// If the [`ActiveStatusEffect`] is already active: + /// 1. if the new effect is the same as the old one and its duration is + /// longer, it replaces the old effect. Otherwise, it does nothing. + /// 2. if the new effect is stronger than the old one: + /// a. if the new effect's duration is longer, it replaces the old effect. + /// b. if the new effect's duration is shorter, it overrides the old + /// 3. if the new effect is weaker than the old one and if the new effect's + /// duration is longer, it will be overridden by the old effect until the + /// old effect's duration is over. + pub fn apply(&mut self, effect: ActiveStatusEffect) { + self.changes.push(StatusEffectChange::Apply(effect)); + } + + /// Replace an existing [`ActiveStatusEffect`]. + pub fn replace(&mut self, effect: ActiveStatusEffect) { + self.changes.push(StatusEffectChange::Replace(effect)); + } + + /// Removes an [`ActiveStatusEffect`]. + pub fn remove(&mut self, effect: StatusEffect) { + self.changes.push(StatusEffectChange::Remove(effect)); + } + + /// Removes all [`ActiveStatusEffect`]s. + pub fn remove_all(&mut self) { + self.changes.push(StatusEffectChange::RemoveAll); + } + + /// Returns true if there are no effects of the given type. + pub fn no_effect(&self, effect: StatusEffect) -> bool { + self.current_effects + .get(&effect) + .map_or(true, |effects| effects.is_empty()) + } + + /// Returns true if there is an effect of the given type. + pub fn has_effect(&self, effect: StatusEffect) -> bool { + self.current_effects + .get(&effect) + .map_or(false, |effects| !effects.is_empty()) + } + + /// Returns true if there are no effects. + pub fn no_effects(&self) -> bool { + self.current_effects.is_empty() + } + + /// Returns true if there are any effects. + pub fn has_effects(&self) -> bool { + !self.current_effects.is_empty() + } + + /// Returns the maximum duration of the given effect. + pub fn max_duration(&self, effect: StatusEffect) -> DurationResult { + let effects = self.current_effects.get(&effect); + + match effects { + None => DurationResult::NoEffects, + Some(effects) => { + if let Some(effect) = effects.last() { + match effect.remaining_duration() { + None => DurationResult::Infinite, + Some(duration) => DurationResult::Finite(duration), + } + } else { + DurationResult::NoEffects + } + } + } + } + + /// Gets the current effect of the given type. + pub fn get_current_effect(&self, effect: StatusEffect) -> Option<&ActiveStatusEffect> { + self.current_effects + .get(&effect) + .and_then(|effects| effects.first()) + } + + /// Gets all the effects of the given type. + pub fn get_all_effect(&self, effect: StatusEffect) -> Option<&Vec> { + self.current_effects.get(&effect) + } + + /// Gets all the current effects. + pub fn get_current_effects(&self) -> Vec<&ActiveStatusEffect> { + self.current_effects + .values() + .filter_map(|effects| effects.first()) + .collect() + } + + /// Gets all the effects. + pub fn get_all_effects(&self) -> &IndexMap> { + &self.current_effects + } +} + +// internal methods +impl ActiveStatusEffects { + /// Applies an effect. + /// + /// The vec must always be sorted in descending order of amplifier and + /// ascending order of duration. + /// + /// Returns true if the effect was applied. + fn apply_effect(&mut self, effect: ActiveStatusEffect) -> bool { + let effects = self + .current_effects + .entry(effect.status_effect()) + .or_default(); + + let duration = effect.remaining_duration(); + let amplifier = effect.amplifier(); + + if let Some(index) = effects.iter().position(|e| e.amplifier() <= amplifier) { + // Found an effect with the same or a lower amplifier. + + let active_status_effect = &effects[index]; + + if active_status_effect.remaining_duration() < duration + || active_status_effect.amplifier() < amplifier + { + // if its duration is shorter or its amplifier is lower, override it. + effects[index] = effect; + + // Remove effects after the current one that have a lower + // duration. + let mut remaining_effects = effects.split_off(index + 1); + remaining_effects.retain(|e| e.remaining_duration() >= duration); + effects.append(&mut remaining_effects); + true + } else if active_status_effect.remaining_duration() > duration + && active_status_effect.amplifier() < amplifier + { + // if its duration is longer and its amplifier is lower, insert + // the new effect before it. + effects.insert(index, effect); + true + } else { + // if its duration is longer and its amplifier is higher, do + // nothing. + false + } + } else { + // Found no effect with an equal or lower amplifier. + // This means all effects have a higher amplifier or the vec is + // empty. + + if let Some(last) = effects.last() { + // There is at least one effect with a higher amplifier. + if last.remaining_duration() < effect.remaining_duration() { + // if its duration is shorter, we can insert it at the end. + effects.push(effect); + true + } else { + // if its duration is longer, do nothing. + false + } + } else { + // The vec is empty. + effects.push(effect); + true + } + } + } + + /// Replaces an effect. + fn replace_effect(&mut self, effect: ActiveStatusEffect) { + self.current_effects + .insert(effect.status_effect(), vec![effect]); + } + + /// Removes an effect. + fn remove_effect(&mut self, effect: StatusEffect) { + self.current_effects.remove(&effect); + } + + /// Removes all effects. + fn remove_all_effects(&mut self) { + self.current_effects.clear(); + } + + /// Removes the strongest effect of the given type, i.e., the first effect + fn remove_strongest_effect(&mut self, effect: StatusEffect) { + if let Some(effects) = self.current_effects.get_mut(&effect) { + effects.remove(0); + } + } + + /// **For internal use only.** + /// + /// Increments the active tick of all effects by a tick. + #[doc(hidden)] + pub fn increment_active_ticks(&mut self) { + for effects in self.current_effects.values_mut() { + for effect in effects.iter_mut() { + effect.increment_active_ticks(); + + if effect.expired() { + self.changes + .push(StatusEffectChange::Expire(effect.status_effect())); + } + } + } + } + + /// **For internal use only.** + /// + /// Applies all the changes. + /// + /// Returns a [`IndexMap`] of [`StatusEffect`]s that were updated or removed + /// and their previous values. + #[doc(hidden)] + pub fn apply_changes(&mut self) -> IndexMap> { + let current = self.current_effects.clone(); + let find_current = |effect: StatusEffect| { + current + .iter() + .find(|e| *e.0 == effect) + .map(|e| e.1.first().cloned())? + }; + let mut updated_effects = IndexMap::new(); + + for change in std::mem::take(&mut self.changes) { + match change { + StatusEffectChange::Apply(effect) => { + let value = effect.status_effect(); + if self.apply_effect(effect) { + updated_effects + .entry(value) + .or_insert_with(|| find_current(value)); + } + } + StatusEffectChange::Replace(effect) => { + let value = effect.status_effect(); + updated_effects + .entry(value) + .or_insert_with(|| find_current(value)); + self.replace_effect(effect); + } + StatusEffectChange::Remove(effect) => { + self.remove_effect(effect); + updated_effects.insert(effect, find_current(effect)); + } + StatusEffectChange::RemoveAll => { + self.remove_all_effects(); + for (status, effects) in current.iter() { + if let Some(effect) = effects.first() { + updated_effects.insert(*status, Some(effect.clone())); + } + } + } + StatusEffectChange::Expire(effect) => { + self.remove_strongest_effect(effect); + updated_effects.insert(effect, find_current(effect)); + } + } + } + + updated_effects + } +} + +/// Represents an active status effect. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct ActiveStatusEffect { + effect: StatusEffect, + /// # Default Value + /// 0 + amplifier: u8, + /// The initial duration of the effect in ticks. + /// If `None`, the effect is infinite. + /// + /// # Default Value + /// Some(600) (30 seconds) + initial_duration: Option, + /// The amount of ticks the effect has been active. + /// + /// # Default Value + /// 0 + active_ticks: i32, + /// # Default Value + /// false + ambient: bool, + /// # Default Value + /// true + show_particles: bool, + /// # Default Value + /// true + show_icon: bool, +} + +impl ActiveStatusEffect { + /// Creates a new [`ActiveStatusEffect`]. + pub fn from_effect(effect: StatusEffect) -> Self { + Self { + effect, + amplifier: 0, + initial_duration: Some(600), + active_ticks: 0, + ambient: false, + show_particles: true, + show_icon: true, + } + } + + /// Sets the amplifier of the [`ActiveStatusEffect`]. + pub fn with_amplifier(mut self, amplifier: u8) -> Self { + self.amplifier = amplifier; + self + } + + /// Sets the duration of the [`ActiveStatusEffect`] in ticks. + pub fn with_duration(mut self, duration: i32) -> Self { + self.initial_duration = Some(duration); + self + } + + /// Sets the duration of the [`ActiveStatusEffect`] in seconds. + pub fn with_duration_seconds(mut self, duration: f32) -> Self { + self.initial_duration = Some((duration * 20.0).round() as i32); + self + } + + /// Sets the duration of the [`ActiveStatusEffect`] to infinite. + pub fn with_infinite(mut self) -> Self { + self.initial_duration = None; + self + } + + /// Sets whether the [`ActiveStatusEffect`] is ambient. + pub fn with_ambient(mut self, ambient: bool) -> Self { + self.ambient = ambient; + self + } + + /// Sets whether the [`ActiveStatusEffect`] shows particles. + pub fn with_show_particles(mut self, show_particles: bool) -> Self { + self.show_particles = show_particles; + self + } + + /// Sets whether the [`ActiveStatusEffect`] shows an icon. + pub fn with_show_icon(mut self, show_icon: bool) -> Self { + self.show_icon = show_icon; + self + } + + /// Increments the active ticks of the [`ActiveStatusEffect`] by one. + pub fn increment_active_ticks(&mut self) { + self.active_ticks += 1; + } + + /// Returns the [`StatusEffect`] of the [`ActiveStatusEffect`]. + pub fn status_effect(&self) -> StatusEffect { + self.effect + } + + /// Returns the amplifier of the [`ActiveStatusEffect`]. + pub fn amplifier(&self) -> u8 { + self.amplifier + } + + /// Returns the initial duration of the [`ActiveStatusEffect`] in ticks. + /// Returns `None` if the [`ActiveStatusEffect`] is infinite. + pub fn initial_duration(&self) -> Option { + self.initial_duration + } + + /// Returns the remaining duration of the [`ActiveStatusEffect`] in ticks. + /// Returns `None` if the [`ActiveStatusEffect`] is infinite. + pub fn remaining_duration(&self) -> Option { + self.initial_duration + .map(|duration| duration - self.active_ticks) + } + + /// Returns the active ticks of the [`ActiveStatusEffect`]. + pub fn active_ticks(&self) -> i32 { + self.active_ticks + } + + /// Returns true if the [`ActiveStatusEffect`] is ambient. + pub fn ambient(&self) -> bool { + self.ambient + } + + /// Returns true if the [`ActiveStatusEffect`] shows particles. + pub fn show_particles(&self) -> bool { + self.show_particles + } + + /// Returns true if the [`ActiveStatusEffect`] shows an icon. + pub fn show_icon(&self) -> bool { + self.show_icon + } + + /// Returns true if the [`ActiveStatusEffect`] has expired or if it is + /// instant. + pub fn expired(&self) -> bool { + self.status_effect().instant() + || self + .remaining_duration() + .map_or(false, |duration| duration <= 0) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_apply_effect() { + let mut effects = ActiveStatusEffects::default(); + + let effect = ActiveStatusEffect::from_effect(StatusEffect::Speed).with_amplifier(1); + let effect2 = ActiveStatusEffect::from_effect(StatusEffect::Speed).with_amplifier(2); + + let effect3 = ActiveStatusEffect::from_effect(StatusEffect::Strength).with_amplifier(1); + let effect4 = ActiveStatusEffect::from_effect(StatusEffect::Strength).with_amplifier(2); + + effects.apply(effect.clone()); + effects.apply_changes(); + assert_eq!( + effects.get_all_effect(StatusEffect::Speed), + Some(&vec![effect.clone()]) + ); + + effects.apply(effect2.clone()); + effects.apply_changes(); + assert_eq!( + effects.get_all_effect(StatusEffect::Speed), + Some(&vec![effect2.clone()]) + ); + + effects.apply(effect3.clone()); + effects.apply_changes(); + assert_eq!( + effects.get_all_effect(StatusEffect::Strength), + Some(&vec![effect3.clone()]) + ); + + effects.apply(effect4.clone()); + effects.apply_changes(); + assert_eq!( + effects.get_all_effect(StatusEffect::Strength), + Some(&vec![effect4.clone()]) + ); + } + + #[test] + fn test_apply_effect_duration() { + let mut effects = ActiveStatusEffects::default(); + + let effect = ActiveStatusEffect::from_effect(StatusEffect::Speed) + .with_amplifier(1) + .with_duration(100); + let effect2 = ActiveStatusEffect::from_effect(StatusEffect::Speed) + .with_amplifier(1) + .with_duration(200); + let effect3 = ActiveStatusEffect::from_effect(StatusEffect::Speed) + .with_amplifier(0) + .with_duration(300); + + effects.apply(effect.clone()); + effects.apply_changes(); + assert_eq!( + effects.get_all_effect(StatusEffect::Speed), + Some(&vec![effect.clone()]) + ); + + effects.apply(effect2.clone()); + effects.apply_changes(); + assert_eq!( + effects.get_all_effect(StatusEffect::Speed), + Some(&vec![effect2.clone()]) + ); + + effects.apply(effect3.clone()); + effects.apply_changes(); + assert_eq!( + effects.get_all_effect(StatusEffect::Speed), + Some(&vec![effect2.clone(), effect3.clone()]) + ); + } +} diff --git a/crates/valence_entity/src/lib.rs b/crates/valence_entity/src/lib.rs index a0329d772..da32fc9e5 100644 --- a/crates/valence_entity/src/lib.rs +++ b/crates/valence_entity/src/lib.rs @@ -18,6 +18,7 @@ )] #![allow(clippy::type_complexity)] +pub mod active_status_effects; pub mod attributes; mod flags; pub mod hitbox; @@ -39,6 +40,7 @@ use valence_server_common::{Despawned, UniqueId}; use crate::attributes::TrackedEntityAttributes; include!(concat!(env!("OUT_DIR"), "/entity.rs")); + pub struct EntityPlugin; /// When new Minecraft entities are initialized and added to diff --git a/crates/valence_server/src/lib.rs b/crates/valence_server/src/lib.rs index b84a8df7b..68291b8bc 100644 --- a/crates/valence_server/src/lib.rs +++ b/crates/valence_server/src/lib.rs @@ -39,6 +39,7 @@ pub mod op_level; pub mod resource_pack; pub mod spawn; pub mod status; +pub mod status_effect; pub mod teleport; pub mod title; diff --git a/crates/valence_server/src/status_effect.rs b/crates/valence_server/src/status_effect.rs new file mode 100644 index 000000000..c1de795f7 --- /dev/null +++ b/crates/valence_server/src/status_effect.rs @@ -0,0 +1,194 @@ +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use bevy_ecs::query::WorldQuery; +use bevy_ecs::system::SystemState; +use valence_entity::active_status_effects::{ActiveStatusEffect, ActiveStatusEffects}; +use valence_entity::entity::Flags; +use valence_entity::living::{PotionSwirlsAmbient, PotionSwirlsColor}; +use valence_protocol::packets::play::{ + entity_status_effect_s2c, EntityStatusEffectS2c, RemoveEntityStatusEffectS2c, +}; +use valence_protocol::status_effects::StatusEffect; +use valence_protocol::{VarInt, WritePacket}; + +use crate::client::Client; +use crate::EventLoopPostUpdate; + +/// Event for when a status effect is added to an entity or the amplifier or +/// duration of an existing status effect is changed. +#[derive(Event, Clone, PartialEq, Eq, Debug)] +pub struct StatusEffectAdded { + pub entity: Entity, + pub status_effect: StatusEffect, +} + +/// Event for when a status effect is removed from an entity. +#[derive(Event, Clone, PartialEq, Eq, Debug)] +pub struct StatusEffectRemoved { + pub entity: Entity, + pub status_effect: ActiveStatusEffect, +} + +pub struct StatusEffectPlugin; + +impl Plugin for StatusEffectPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_systems( + EventLoopPostUpdate, + ( + add_status_effects, + update_active_status_effects, + add_status_effects, + ), + ); + } +} + +fn update_active_status_effects( + world: &mut World, + state: &mut SystemState>, +) { + let mut query = state.get_mut(world); + for mut active_status_effects in query.iter_mut() { + active_status_effects.increment_active_ticks(); + } +} + +fn create_packet(effect: &ActiveStatusEffect) -> EntityStatusEffectS2c { + EntityStatusEffectS2c { + entity_id: VarInt(0), // We reserve ID 0 for clients. + effect_id: VarInt(effect.status_effect().to_raw() as i32), + amplifier: effect.amplifier(), + duration: VarInt(effect.remaining_duration().unwrap_or(-1)), + flags: entity_status_effect_s2c::Flags::new() + .with_is_ambient(effect.ambient()) + .with_show_particles(effect.show_particles()) + .with_show_icon(effect.show_icon()), + factor_codec: None, + } +} + +#[derive(WorldQuery)] +#[world_query(mutable)] +struct StatusEffectQuery { + entity: Entity, + active_effects: &'static mut ActiveStatusEffects, + client: Option<&'static mut Client>, + entity_flags: Option<&'static mut Flags>, + swirl_color: Option<&'static mut PotionSwirlsColor>, + swirl_ambient: Option<&'static mut PotionSwirlsAmbient>, +} + +fn add_status_effects( + mut query: Query, + mut add_events: EventWriter, + mut remove_events: EventWriter, +) { + for mut query in query.iter_mut() { + let updated = query.active_effects.apply_changes(); + + if updated.is_empty() { + continue; + } + + set_swirl( + &query.active_effects, + &mut query.swirl_color, + &mut query.swirl_ambient, + ); + + for (status_effect, prev) in updated { + if query.active_effects.has_effect(status_effect) { + add_events.send(StatusEffectAdded { + entity: query.entity, + status_effect, + }); + } else if let Some(prev) = prev { + remove_events.send(StatusEffectRemoved { + entity: query.entity, + status_effect: prev, + }); + } else { + // this should never happen + panic!("status effect was removed but was never added"); + } + + update_status_effect(&mut query, status_effect); + } + } +} + +fn update_status_effect(query: &mut StatusEffectQueryItem, status_effect: StatusEffect) { + let current_effect = query.active_effects.get_current_effect(status_effect); + + if let Some(ref mut client) = query.client { + if let Some(updated_effect) = current_effect { + client.write_packet(&create_packet(updated_effect)); + } else { + client.write_packet(&RemoveEntityStatusEffectS2c { + entity_id: VarInt(0), + effect_id: VarInt(status_effect.to_raw() as i32), + }); + } + } +} + +fn set_swirl( + active_status_effects: &ActiveStatusEffects, + swirl_color: &mut Option>, + swirl_ambient: &mut Option>, +) { + if let Some(ref mut swirl_ambient) = swirl_ambient { + swirl_ambient.0 = active_status_effects + .get_current_effects() + .iter() + .any(|effect| effect.ambient()); + } + + if let Some(ref mut swirl_color) = swirl_color { + swirl_color.0 = get_color(active_status_effects); + } +} + +/// Used to set the color of the swirls in the potion effect. +/// +/// Equivalent to net.minecraft.potion.PotionUtil#getColor +fn get_color(effects: &ActiveStatusEffects) -> i32 { + if effects.no_effects() { + // vanilla mc seems to return 0x385dc6 if there are no effects + // dunno why + // imma just say to return 0 to remove the swirls + return 0; + } + + let effects = effects.get_current_effects(); + let mut f = 0.0; + let mut g = 0.0; + let mut h = 0.0; + let mut j = 0.0; + + for status_effect_instance in effects { + if !status_effect_instance.show_particles() { + continue; + } + + let k = status_effect_instance.status_effect().color(); + let l = (status_effect_instance.amplifier() + 1) as f32; + f += (l * ((k >> 16) & 0xff) as f32) / 255.0; + g += (l * ((k >> 8) & 0xff) as f32) / 255.0; + h += (l * ((k) & 0xff) as f32) / 255.0; + j += l; + } + + if j == 0.0 { + return 0; + } + + f = f / j * 255.0; + g = g / j * 255.0; + h = h / j * 255.0; + + ((f as i32) << 16) | ((g as i32) << 8) | (h as i32) +} diff --git a/examples/potions.rs b/examples/potions.rs new file mode 100644 index 000000000..607610a9b --- /dev/null +++ b/examples/potions.rs @@ -0,0 +1,334 @@ +use rand::seq::SliceRandom; +use rand::Rng; +use valence::client::despawn_disconnected_clients; +use valence::entity::active_status_effects::{ActiveStatusEffect, ActiveStatusEffects}; +use valence::log::LogPlugin; +use valence::network::ConnectionMode; +use valence::prelude::*; +use valence::status_effects::{AttributeModifier, StatusEffect}; +use valence_server::entity::attributes::{EntityAttribute, EntityAttributes}; +use valence_server::entity::entity::Flags; +use valence_server::entity::living::{Absorption, Health}; +use valence_server::status_effect::{StatusEffectAdded, StatusEffectRemoved}; + +const SPAWN_Y: i32 = 64; + +// Notes: Some potion effects are implemented by the client (i.e. we don't need +// to send any more packets than just telling the client about them) and some +// are implemented by the server. The ones implemented by the client are: +// - Jump Boost +// - Night Vision +// - Nausea +// - Blindness +// - Darkness +// - Slow Falling +// - Levitation +// Perhaps also (haven't tested): +// - Dolphin's Grace +// - Conduit Power +// +// There are also a few different potion effects that are implemented by the +// server. Some can be implemented right now, for example: +// - Speed +// - Instant Health +// - Regeneration +// - Absorption +// - Glowing +// - etc. (i.e. the ones with AttributeModifiers, direct health changes or other +// trivial effects) +// +// Some can't be implemented right now because they require features that aren't +// implemented yet or must be implemented yourself, for example: +// - Water Breathing (requires the ability to breathe underwater) +// - Fire Resistance (requires the ability to not take damage from fire) +// - Hunger (requires the ability to get hungry) +// - Bad Omen (requires the ability to get a raid) +fn main() { + App::new() + .insert_resource(NetworkSettings { + connection_mode: ConnectionMode::Offline, + ..Default::default() + }) + .add_plugins(DefaultPlugins.build().disable::()) + .add_systems(Startup, setup) + .add_systems( + EventLoopUpdate, + ( + add_potion_effect, + handle_status_effect_added, + handle_status_effect_removed, + ), + ) + .add_systems( + Update, + ( + init_clients, + despawn_disconnected_clients, + handle_status_effect_update, + ), + ) + .run(); +} + +fn setup( + mut commands: Commands, + server: Res, + biomes: Res, + dimensions: Res, +) { + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -5..5 { + for x in -5..5 { + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + } + } + + for z in -25..25 { + for x in -25..25 { + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + commands.spawn(layer); +} + +#[allow(clippy::type_complexity)] +fn init_clients( + mut clients: Query< + ( + &mut Client, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, +) { + for ( + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + *game_mode = GameMode::Survival; + + client.send_chat_message("Welcome to the potions example.".bold()); + client.send_chat_message("Sneak to apply a random potion effect.".into_text()); + client.send_chat_message("Note: Some potion effects are not implemented yet.".into_text()); + } +} + +pub fn add_potion_effect( + mut clients: Query<&mut ActiveStatusEffects>, + mut events: EventReader, +) { + let mut rng = rand::thread_rng(); + for event in events.read() { + if event.state == SneakState::Start { + if let Ok(mut status) = clients.get_mut(event.client) { + status.apply( + ActiveStatusEffect::from_effect(*StatusEffect::ALL.choose(&mut rng).unwrap()) + .with_duration(rng.gen_range(10..1000)) + .with_amplifier(rng.gen_range(0..5)), + ); + } + } + } +} + +fn adjust_modifier_amount(amplifier: u8, amount: f64) -> f64 { + amount * (amplifier + 1) as f64 +} + +fn apply_potion_attribute( + attributes: &mut Mut, + health: &mut Option>, + amplifier: u8, + attr: AttributeModifier, +) { + attributes.remove_modifier(attr.attribute, attr.uuid); + + let amount = adjust_modifier_amount(amplifier, attr.value); + + attributes.set_modifier(attr.attribute, attr.uuid, amount, attr.operation); + + // not quite how vanilla does it, but it's close enough + if attr.attribute == EntityAttribute::GenericMaxHealth { + if let Some(ref mut health) = health { + health.0 = health.0.min( + attributes + .get_compute_value(EntityAttribute::GenericMaxHealth) + .unwrap_or(0.0) as f32, + ); + } + } +} + +fn remove_potion_attribute( + attributes: &mut Mut, + health: &mut Option>, + attr: AttributeModifier, +) { + attributes.remove_modifier(attr.attribute, attr.uuid); + + if attr.attribute == EntityAttribute::GenericMaxHealth { + if let Some(ref mut health) = health { + health.0 = health.0.min( + attributes + .get_compute_value(EntityAttribute::GenericMaxHealth) + .unwrap_or(0.0) as f32, + ); + } + } +} + +#[allow(clippy::type_complexity)] +pub fn handle_status_effect_added( + mut clients: Query<( + &ActiveStatusEffects, + &mut EntityAttributes, + Option<&mut Health>, + Option<&mut Absorption>, + &mut Flags, + )>, + mut events: EventReader, +) { + for event in events.read() { + if let Ok((status, mut attributes, mut health, absorption, mut flags)) = + clients.get_mut(event.entity) + { + let effect = status.get_current_effect(event.status_effect).unwrap(); + + match event.status_effect { + StatusEffect::Absorption => { + // not quite how vanilla does it. if you want to do it the vanilla way, you'll + // need to keep track of the previous absorption value and subtract that from + // the new value (because you can take damage while having absorption) + if let Some(mut absorption) = absorption { + absorption.0 += (effect.amplifier() + 1) as f32 * 4.0; + } + } + StatusEffect::InstantHealth => { + if let Some(mut health) = health { + health.0 += (4 << effect.amplifier().min(31)) as f32; + } + } + StatusEffect::InstantDamage => { + if let Some(mut health) = health { + health.0 -= (6 << effect.amplifier().min(31)) as f32; + } + } + StatusEffect::Glowing => { + flags.set_glowing(true); + } + StatusEffect::Invisibility => { + flags.set_invisible(true); + } + status => { + for attr in status.attribute_modifiers() { + apply_potion_attribute( + &mut attributes, + &mut health, + effect.amplifier(), + attr, + ); + } + } + } + } + } +} + +pub fn handle_status_effect_removed( + mut clients: Query<( + &mut EntityAttributes, + Option<&mut Health>, + Option<&mut Absorption>, + &mut Flags, + )>, + mut events: EventReader, +) { + for event in events.read() { + if let Ok((mut attributes, mut health, absorption, mut flags)) = + clients.get_mut(event.entity) + { + let effect = &event.status_effect; + match effect.status_effect() { + StatusEffect::Absorption => { + if let Some(mut absorption) = absorption { + absorption.0 -= (effect.amplifier() + 1) as f32 * 4.0; + } + } + StatusEffect::Glowing => { + flags.set_glowing(false); + } + StatusEffect::Invisibility => { + flags.set_invisible(false); + } + status => { + for attr in status.attribute_modifiers() { + remove_potion_attribute(&mut attributes, &mut health, attr); + } + } + } + } + } +} + +pub fn handle_status_effect_update( + mut clients: Query<(&ActiveStatusEffects, &EntityAttributes, Option<&mut Health>)>, +) { + for (status, attributes, mut health) in &mut clients.iter_mut() { + for effect in status.get_current_effects() { + match effect.status_effect() { + StatusEffect::Regeneration => { + let i = 50 >> effect.amplifier().min(31) as u32; + + if i == 0 || effect.active_ticks() % i == 0 { + if let Some(ref mut health) = health { + health.0 = (health.0 + 1.0).min( + attributes + .get_compute_value(EntityAttribute::GenericMaxHealth) + .unwrap_or(0.0) as f32, + ); + } + } + } + StatusEffect::Poison => { + let i = 25 >> effect.amplifier().min(31) as u32; + + if i == 0 || effect.active_ticks() % i == 0 { + if let Some(ref mut health) = health { + health.0 = (health.0 - 1.0).max(1.0); + } + } + } + StatusEffect::Wither => { + let i = 40 >> effect.amplifier().min(31) as u32; + + if i == 0 || effect.active_ticks() % i == 0 { + if let Some(ref mut health) = health { + health.0 = (health.0 - 1.0).max(0.0); + } + } + } + _ => {} + } + } + } +} diff --git a/extractor/src/main/java/rs/valence/extractor/extractors/Effects.java b/extractor/src/main/java/rs/valence/extractor/extractors/Effects.java index b6046631d..c89de4245 100644 --- a/extractor/src/main/java/rs/valence/extractor/extractors/Effects.java +++ b/extractor/src/main/java/rs/valence/extractor/extractors/Effects.java @@ -52,4 +52,4 @@ public JsonElement extract() { return effectsJson; } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index d9a7b58e3..7a3dbfd8e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,7 @@ use valence_server::op_level::OpLevelPlugin; pub use valence_server::protocol::status_effects; use valence_server::resource_pack::ResourcePackPlugin; use valence_server::status::StatusPlugin; +use valence_server::status_effect::StatusEffectPlugin; use valence_server::teleport::TeleportPlugin; pub use valence_server::*; #[cfg(feature = "weather")] @@ -194,6 +195,7 @@ impl PluginGroup for DefaultPlugins { .add(OpLevelPlugin) .add(ResourcePackPlugin) .add(StatusPlugin) + .add(StatusEffectPlugin) .add(AbilitiesPlugin); #[cfg(feature = "log")] diff --git a/src/tests.rs b/src/tests.rs index 4534f8f42..e82971210 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -5,6 +5,7 @@ mod hunger; mod inventory; mod layer; mod player_list; +mod potions; mod scoreboard; mod weather; mod world_border; diff --git a/src/tests/potions.rs b/src/tests/potions.rs new file mode 100644 index 000000000..fe0a34a1d --- /dev/null +++ b/src/tests/potions.rs @@ -0,0 +1,71 @@ +use valence_server::entity::active_status_effects::{ActiveStatusEffect, ActiveStatusEffects}; +use valence_server::protocol::packets::play::{EntityStatusEffectS2c, RemoveEntityStatusEffectS2c}; +use valence_server::protocol::status_effects::StatusEffect; +use valence_server::protocol::VarInt; + +use crate::testing::ScenarioSingleClient; + +#[test] +fn test_status_effects_packets() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + // Add a potion effect to the client. + let mut effects = app + .world + .get_mut::(client) + .expect("Client should have status effects"); + effects.apply( + ActiveStatusEffect::from_effect(StatusEffect::BadOmen) + .with_duration(100) + .with_amplifier(1), + ); + + // Update the server. + app.update(); + + // Make assertions + let sent_packets = helper.collect_received(); + + sent_packets.assert_count::(1); + + let packet = sent_packets.first::(); + + assert_eq!(packet.entity_id, VarInt(0)); // Client entity ID is always 0 + assert_eq!(packet.effect_id, VarInt(31)); // Bad Omen + assert_eq!(packet.amplifier, 1); + assert_eq!(packet.duration, VarInt(100)); + + // Clear the potion effect + for _ in 0..99 { + app.update(); + } + + helper.clear_received(); + app.update(); + + // Make assertions + let effects = app + .world + .get::(client) + .expect("Client should have status effects"); + + assert_eq!(effects.get_current_effect(StatusEffect::BadOmen), None); + + let sent_packets = helper.collect_received(); + + sent_packets.assert_count::(1); + + let packet = sent_packets.first::(); + + assert_eq!(packet.entity_id, VarInt(0)); // Client entity ID is always 0 + assert_eq!(packet.effect_id, VarInt(31)); // Bad Omen +}