aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2026-03-19 04:12:20 -0100
committermat <git@matdoes.dev>2026-03-19 04:12:20 -0100
commitb03d2942e1bef98e13acadde5cbb8856a3f8c74d (patch)
treef1be3cd3151c3194677001d520aedf2ad18f92b8
parent176907fa8bc2c03f245b837f09a80d073856d4dd (diff)
downloadazalea-drasl-b03d2942e1bef98e13acadde5cbb8856a3f8c74d.tar.xz
implement speed effect
-rw-r--r--CHANGELOG.md1
-rw-r--r--azalea-client/src/plugins/chat/mod.rs12
-rw-r--r--azalea-client/src/plugins/packet/game/mod.rs110
-rw-r--r--azalea-entity/src/effects.rs131
-rw-r--r--azalea-entity/src/plugin/effect_events.rs58
-rw-r--r--azalea-entity/src/plugin/mod.rs4
-rw-r--r--azalea/examples/testbot/commands.rs3
-rw-r--r--azalea/examples/testbot/commands/debug.rs8
-rw-r--r--azalea/examples/testbot/main.rs2
-rw-r--r--azalea/src/client_impl/chat.rs2
10 files changed, 265 insertions, 66 deletions
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::<ActiveEffects>() {
- 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::<ActiveEffects>() {
- 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<MobEffect, MobEffectData>);
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<MobEffectData> {
+ self.0.insert(effect, data)
}
pub fn remove(&mut self, effect: MobEffect) -> Option<MobEffectData> {
@@ -73,7 +78,7 @@ impl ActiveEffects {
}
/// Get the amplifier level for the effect, starting at 0.
- pub fn get_level(&self, effect: MobEffect) -> Option<u32> {
+ pub fn get_level(&self, effect: MobEffect) -> Option<i32> {
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<u32> {
+ pub fn get_dig_speed_amplifier(&self) -> Option<i32> {
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<AddEffectEvent>,
+ 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<MobEffect>,
+}
+pub fn handle_remove_effects(
+ remove_effects: On<RemoveEffectsEvent>,
+ 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::<EntityUuidIndex>();
}
}
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<String>) {
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<Mutex<CommandSource>>) {
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<()> {