From b03d2942e1bef98e13acadde5cbb8856a3f8c74d Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 19 Mar 2026 04:12:20 -0100 Subject: implement speed effect --- CHANGELOG.md | 1 + azalea-client/src/plugins/chat/mod.rs | 12 ++- azalea-client/src/plugins/packet/game/mod.rs | 110 +++++++++++----------- azalea-entity/src/effects.rs | 131 +++++++++++++++++++++++++-- azalea-entity/src/plugin/effect_events.rs | 58 ++++++++++++ azalea-entity/src/plugin/mod.rs | 4 + azalea/examples/testbot/commands.rs | 3 +- azalea/examples/testbot/commands/debug.rs | 8 ++ azalea/examples/testbot/main.rs | 2 +- azalea/src/client_impl/chat.rs | 2 + 10 files changed, 265 insertions(+), 66 deletions(-) create mode 100644 azalea-entity/src/plugin/effect_events.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1edbc492..8ddb1c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ is breaking anyways, semantic versioning is not followed. - `AccountTrait` was implemented, which allows for custom refresh and join behavior for `Account`s. - Add `Account::microsoft_with_opts` to make it easier to create accounts with custom cache files. (@ElijahBare) - Add an `EntityRef` type to simplify interactions with entities. +- Implement speed/swiftness. - Add `BlockTrait::set_property` to allow setting properties on blocks generically. - You can now access a client's XP with `Client::experience`. (@nebula161) - Re-implement `Client::map_component` and `map_get_component`. diff --git a/azalea-client/src/plugins/chat/mod.rs b/azalea-client/src/plugins/chat/mod.rs index 11ad742c..ca247ec4 100644 --- a/azalea-client/src/plugins/chat/mod.rs +++ b/azalea-client/src/plugins/chat/mod.rs @@ -100,6 +100,12 @@ impl ChatPacket { { return (Some(m[1].to_string()), m[2].to_string()); } + // hypixel whispers + if let Some(m) = + regex!(r"^From (?:\[[^\]]+\] )(\w{1,16}): (.+)$").captures(&message) + { + return (Some(m[1].to_string()), m[2].to_string()); + } (None, message) } @@ -171,7 +177,11 @@ impl ChatPacket { if p.overlay { return false; } - if regex!("^(-> me|[a-zA-Z_0-9]{1,16} whispers: )").is_match(&message) { + if regex!(r"^(-> me|\w{1,16} whispers: )").is_match(&message) { + return true; + } + // hypixel + if regex!(r"^From (?:\[[^\]]+\] )?\w{1,16}: ").is_match(&message) { return true; } diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index 16d81eae..81eac46b 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -9,8 +9,9 @@ use azalea_core::{ position::{ChunkPos, Vec3}, }; use azalea_entity::{ - ActiveEffects, Dead, EntityBundle, EntityKindComponent, HasClientLoaded, LoadedBy, LocalEntity, - LookDirection, Physics, PlayerAbilities, Position, + Dead, EntityBundle, EntityKindComponent, HasClientLoaded, LoadedBy, LocalEntity, LookDirection, + Physics, PlayerAbilities, Position, + effect_events::{AddEffectEvent, RemoveEffectsEvent}, indexing::{EntityIdIndex, EntityUuidIndex}, inventory::Inventory, metadata::{Health, apply_metadata}, @@ -1073,38 +1074,36 @@ impl GamePacketHandler<'_> { pub fn update_mob_effect(&mut self, p: &ClientboundUpdateMobEffect) { debug!("Got update mob effect packet {p:?}"); - let mob_effect = p.mob_effect; - let effect_data = &p.data; + as_system::<( + Commands, + Query<(&EntityIdIndex, &WorldHolder)>, + EntityUpdateQuery, + )>(self.ecs, |(mut commands, query, entity_update_query)| { + let (entity_id_index, world_holder) = query.get(self.player).unwrap(); - as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>( - self.ecs, - |(mut commands, query)| { - let (entity_id_index, world_holder) = query.get(self.player).unwrap(); + let Some(entity) = entity_id_index.get_by_minecraft_entity(p.entity_id) else { + debug!( + "Got update mob effect packet for unknown entity id {}", + p.entity_id + ); + return; + }; - let Some(entity) = entity_id_index.get_by_minecraft_entity(p.entity_id) else { - debug!( - "Got update mob effect packet for unknown entity id {}", - p.entity_id - ); - return; - }; + if !should_apply_entity_update( + &mut commands, + &mut world_holder.partial.write(), + entity, + entity_update_query, + ) { + return; + } - let partial_world = world_holder.partial.clone(); - let effect_data = effect_data.clone(); - commands.entity(entity).queue(RelativeEntityUpdate::new( - partial_world, - move |entity| { - if let Some(mut active_effects) = entity.get_mut::() { - active_effects.insert(mob_effect, effect_data.clone()); - } else { - let mut active_effects = ActiveEffects::default(); - active_effects.insert(mob_effect, effect_data.clone()); - entity.insert(active_effects); - } - }, - )); - }, - ); + commands.trigger(AddEffectEvent { + entity, + id: p.mob_effect, + data: p.data.clone(), + }); + }); } pub fn award_stats(&mut self, _p: &ClientboundAwardStats) {} @@ -1316,32 +1315,35 @@ impl GamePacketHandler<'_> { pub fn remove_mob_effect(&mut self, p: &ClientboundRemoveMobEffect) { debug!("Got remove mob effect packet {p:?}"); - let mob_effect = p.effect; + as_system::<( + Commands, + Query<(&EntityIdIndex, &WorldHolder)>, + EntityUpdateQuery, + )>(self.ecs, |(mut commands, query, entity_update_query)| { + let (entity_id_index, world_holder) = query.get(self.player).unwrap(); - as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>( - self.ecs, - |(mut commands, query)| { - let (entity_id_index, world_holder) = query.get(self.player).unwrap(); + let Some(entity) = entity_id_index.get_by_minecraft_entity(p.entity_id) else { + debug!( + "Got remove mob effect packet for unknown entity id {}", + p.entity_id + ); + return; + }; - let Some(entity) = entity_id_index.get_by_minecraft_entity(p.entity_id) else { - debug!( - "Got remove mob effect packet for unknown entity id {}", - p.entity_id - ); - return; - }; + if !should_apply_entity_update( + &mut commands, + &mut world_holder.partial.write(), + entity, + entity_update_query, + ) { + return; + } - let partial_world = world_holder.partial.clone(); - commands.entity(entity).queue(RelativeEntityUpdate::new( - partial_world, - move |entity| { - if let Some(mut active_effects) = entity.get_mut::() { - active_effects.remove(mob_effect); - } - }, - )); - }, - ); + commands.trigger(RemoveEffectsEvent { + entity, + effects: vec![p.effect], + }); + }); } pub fn resource_pack_push(&mut self, p: &ClientboundResourcePackPush) { diff --git a/azalea-entity/src/effects.rs b/azalea-entity/src/effects.rs index 72c757d6..9ce15d88 100644 --- a/azalea-entity/src/effects.rs +++ b/azalea-entity/src/effects.rs @@ -4,17 +4,22 @@ use std::{ }; use azalea_buf::{AzBuf, BufReadError}; -use azalea_core::bitset::FixedBitSet; -use azalea_registry::builtin::MobEffect; +use azalea_core::{attribute_modifier_operation::AttributeModifierOperation, bitset::FixedBitSet}; +use azalea_inventory::components::AttributeModifier; +use azalea_registry::{ + builtin::{Attribute, MobEffect}, + identifier::Identifier, +}; /// Data about an active mob effect. #[derive(AzBuf, Clone, Debug, Default, PartialEq)] pub struct MobEffectData { /// The effect's amplifier level, starting at 0 if present. #[var] - pub amplifier: u32, + pub amplifier: i32, + /// The effect's duration in ticks. #[var] - pub duration_ticks: u32, + pub duration: i32, pub flags: MobEffectFlags, } @@ -64,8 +69,8 @@ impl AzBuf for MobEffectFlags { #[derive(Clone, Debug, Default)] pub struct ActiveEffects(pub HashMap); impl ActiveEffects { - pub fn insert(&mut self, effect: MobEffect, data: MobEffectData) { - self.0.insert(effect, data); + pub fn insert(&mut self, effect: MobEffect, data: MobEffectData) -> Option { + self.0.insert(effect, data) } pub fn remove(&mut self, effect: MobEffect) -> Option { @@ -73,7 +78,7 @@ impl ActiveEffects { } /// Get the amplifier level for the effect, starting at 0. - pub fn get_level(&self, effect: MobEffect) -> Option { + pub fn get_level(&self, effect: MobEffect) -> Option { self.0.get(&effect).map(|data| data.amplifier) } @@ -82,7 +87,7 @@ impl ActiveEffects { } /// Returns the amplifier for dig speed (haste / conduit power), if present. - pub fn get_dig_speed_amplifier(&self) -> Option { + pub fn get_dig_speed_amplifier(&self) -> Option { let haste_level = self .get_level(MobEffect::Haste) .map(|level| level + 1) @@ -92,7 +97,7 @@ impl ActiveEffects { .map(|level| level + 1) .unwrap_or_default(); - let effect_plus_one = u32::max(haste_level, conduit_power_level); + let effect_plus_one = i32::max(haste_level, conduit_power_level); if effect_plus_one > 0 { Some(effect_plus_one - 1) } else { @@ -100,3 +105,111 @@ impl ActiveEffects { } } } + +pub fn attribute_modifier_for_effect(id: MobEffect) -> Option<(Attribute, AttributeTemplate)> { + Some(match id { + MobEffect::Speed => ( + Attribute::MovementSpeed, + AttributeTemplate::new( + "effect.speed", + 0.2f32 as f64, + AttributeModifierOperation::AddMultipliedTotal, + ), + ), + MobEffect::Slowness => ( + Attribute::MovementSpeed, + AttributeTemplate::new( + "effect.slowness", + -0.15f32 as f64, + AttributeModifierOperation::AddMultipliedTotal, + ), + ), + MobEffect::Haste => ( + Attribute::AttackSpeed, + AttributeTemplate::new( + "effect.haste", + 0.1f32 as f64, + AttributeModifierOperation::AddMultipliedTotal, + ), + ), + MobEffect::MiningFatigue => ( + Attribute::AttackSpeed, + AttributeTemplate::new( + "effect.mining_fatigue", + -0.1f32 as f64, + AttributeModifierOperation::AddMultipliedTotal, + ), + ), + MobEffect::Strength => ( + Attribute::AttackDamage, + AttributeTemplate::new("effect.strength", 3.0, AttributeModifierOperation::AddValue), + ), + MobEffect::JumpBoost => ( + Attribute::SafeFallDistance, + AttributeTemplate::new( + "effect.jump_boost", + 1.0, + AttributeModifierOperation::AddValue, + ), + ), + MobEffect::Invisibility => ( + Attribute::WaypointTransmitRange, + AttributeTemplate::new( + "effect.waypoint_transmit_range_hide", + -1.0, + AttributeModifierOperation::AddMultipliedTotal, + ), + ), + MobEffect::Weakness => ( + Attribute::AttackDamage, + AttributeTemplate::new( + "effect.weakness", + -4.0, + AttributeModifierOperation::AddValue, + ), + ), + MobEffect::HealthBoost => ( + Attribute::MaxHealth, + AttributeTemplate::new( + "effect.health_boost", + 4.0, + AttributeModifierOperation::AddValue, + ), + ), + MobEffect::Absorption => ( + Attribute::MaxAbsorption, + AttributeTemplate::new( + "effect.absorption", + 4.0, + AttributeModifierOperation::AddValue, + ), + ), + MobEffect::Luck => ( + Attribute::Luck, + AttributeTemplate::new("effect.luck", 1.0, AttributeModifierOperation::AddValue), + ), + MobEffect::Unluck => ( + Attribute::Luck, + AttributeTemplate::new("effect.unluck", -1.0, AttributeModifierOperation::AddValue), + ), + _ => return None, + }) +} + +pub struct AttributeTemplate(AttributeModifier); +impl AttributeTemplate { + pub fn new(id: &str, amount: f64, operation: AttributeModifierOperation) -> Self { + Self(AttributeModifier { + id: Identifier::from(id), + amount, + operation, + }) + } + pub fn create(self, amplifier: i32) -> AttributeModifier { + AttributeModifier { + id: self.0.id, + amount: self.0.amount * (amplifier + 1) as f64, + operation: self.0.operation, + } + } +} diff --git a/azalea-entity/src/plugin/effect_events.rs b/azalea-entity/src/plugin/effect_events.rs new file mode 100644 index 00000000..2d37c463 --- /dev/null +++ b/azalea-entity/src/plugin/effect_events.rs @@ -0,0 +1,58 @@ +use azalea_registry::builtin::MobEffect; +use bevy_ecs::{entity::Entity, event::EntityEvent, observer::On, system::Query}; +use tracing::warn; + +use crate::{ActiveEffects, Attributes, MobEffectData, effects::attribute_modifier_for_effect}; + +#[derive(EntityEvent)] +pub struct AddEffectEvent { + pub entity: Entity, + pub id: MobEffect, + pub data: MobEffectData, +} +pub fn handle_add_effect( + add_effect: On, + mut query: Query<(&mut ActiveEffects, &mut Attributes)>, +) { + let Ok((mut active_effects, mut attributes)) = query.get_mut(add_effect.entity) else { + warn!("got handle_add_effect for an entity without the required components"); + return; + }; + + active_effects.insert(add_effect.id, add_effect.data.clone()); + + if let Some((attribute, modifier)) = attribute_modifier_for_effect(add_effect.id) { + let modifier = modifier.create(add_effect.data.amplifier); + if let Some(attribute) = attributes.get_mut(attribute) { + attribute.insert(modifier); + } + } +} + +#[derive(EntityEvent)] +pub struct RemoveEffectsEvent { + pub entity: Entity, + pub effects: Vec, +} +pub fn handle_remove_effects( + remove_effects: On, + mut query: Query<(&mut ActiveEffects, &mut Attributes)>, +) { + let Ok((mut active_effects, mut attributes)) = query.get_mut(remove_effects.entity) else { + warn!("got handle_remove_effects for an entity without the required components"); + return; + }; + + for &effect in &remove_effects.effects { + active_effects.remove(effect); + + if let Some((attribute, modifier)) = attribute_modifier_for_effect(effect) { + // we're just trying to get the id of the modifier, so the amplifier passed here + // doesn't matter + let modifier = modifier.create(0); + if let Some(attribute) = attributes.get_mut(attribute) { + attribute.remove(&modifier.id); + } + } + } +} diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs index 6035f674..4d97080f 100644 --- a/azalea-entity/src/plugin/mod.rs +++ b/azalea-entity/src/plugin/mod.rs @@ -1,4 +1,5 @@ mod components; +pub mod effect_events; pub mod indexing; use std::collections::HashSet; @@ -22,6 +23,7 @@ use crate::{ FluidOnEyes, LookDirection, Physics, Pose, Position, dimensions::{EntityDimensions, calculate_dimensions}, metadata::{self, Health, Player}, + plugin::effect_events::{handle_add_effect, handle_remove_effects}, }; /// A Bevy [`SystemSet`] for various types of entity updates. @@ -65,6 +67,8 @@ impl Plugin for EntityPlugin { ), ) .add_systems(GameTick, (update_in_loaded_chunk, update_fluid_on_eyes)) + .add_observer(handle_add_effect) + .add_observer(handle_remove_effects) .init_resource::(); } } diff --git a/azalea/examples/testbot/commands.rs b/azalea/examples/testbot/commands.rs index 930f41ca..4c0e7235 100644 --- a/azalea/examples/testbot/commands.rs +++ b/azalea/examples/testbot/commands.rs @@ -23,8 +23,9 @@ impl CommandSource { pub fn reply(&self, message: impl Into) { let message = message.into(); if self.chat.is_whisper() { + // /msg instead of /w for compat with custom servers self.bot - .chat(format!("/w {} {message}", self.chat.sender().unwrap())); + .chat(format!("/msg {} {message}", self.chat.sender().unwrap())); } else { self.bot.chat(message); } diff --git a/azalea/examples/testbot/commands/debug.rs b/azalea/examples/testbot/commands/debug.rs index 12f6d362..cd487abb 100644 --- a/azalea/examples/testbot/commands/debug.rs +++ b/azalea/examples/testbot/commands/debug.rs @@ -29,6 +29,14 @@ pub fn register(commands: &mut CommandDispatcher>) { source.reply("pong!"); 1 })); + commands.register( + literal("say").then(argument("message", greedy_string()).executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + let message = get_string(ctx, "message").unwrap(); + source.bot.chat(message); + 1 + })), + ); commands.register(literal("disconnect").executes(|ctx: &Ctx| { let source = ctx.source.lock(); diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs index 63d83c8a..f5f6c096 100644 --- a/azalea/examples/testbot/main.rs +++ b/azalea/examples/testbot/main.rs @@ -120,7 +120,7 @@ pub struct State { impl State { fn new() -> Self { Self { - killaura: true, + killaura: false, task: Arc::new(Mutex::new(BotTask::None)), } } diff --git a/azalea/src/client_impl/chat.rs b/azalea/src/client_impl/chat.rs index e71a208f..1a7359f7 100644 --- a/azalea/src/client_impl/chat.rs +++ b/azalea/src/client_impl/chat.rs @@ -33,6 +33,8 @@ impl Client { /// Send a message in chat. /// + /// If the content starts with a `/`, a command is sent. + /// /// ```rust,no_run /// # use azalea::Client; /// # async fn example(bot: Client) -> anyhow::Result<()> { -- cgit v1.2.3