feat(command): add attributes

This commit is contained in:
Alexander Medvedev 2026-07-13 18:43:45 +02:00
parent ce05798fac
commit 486f8d2cfe
7 changed files with 693 additions and 0 deletions

View File

@ -803,6 +803,7 @@ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
"tracing", "tracing",
"uuid",
] ]
[[package]] [[package]]

View File

@ -25,17 +25,21 @@ pub fn build() -> TokenStream {
sorted_attributes.sort_by_key(|(_, raw)| raw.id); sorted_attributes.sort_by_key(|(_, raw)| raw.id);
let mut constant_defs = Vec::new(); let mut constant_defs = Vec::new();
let mut constant_idents = Vec::new();
for (raw_name, raw_value) in sorted_attributes { for (raw_name, raw_value) in sorted_attributes {
let constant_ident = format_ident!("{}", raw_name.to_shouty_snake_case()); let constant_ident = format_ident!("{}", raw_name.to_shouty_snake_case());
constant_idents.push(constant_ident.clone());
let id_lit = LitInt::new(&raw_value.id.to_string(), Span::call_site()); let id_lit = LitInt::new(&raw_value.id.to_string(), Span::call_site());
let default_value_lit = raw_value.default_value; let default_value_lit = raw_value.default_value;
let name_str = format!("minecraft:{raw_name}");
constant_defs.push(quote!( constant_defs.push(quote!(
pub const #constant_ident: Self = Self { pub const #constant_ident: Self = Self {
id: #id_lit, id: #id_lit,
default_value: #default_value_lit, default_value: #default_value_lit,
name: #name_str,
}; };
)); ));
} }
@ -47,6 +51,7 @@ pub fn build() -> TokenStream {
pub struct Attributes { pub struct Attributes {
pub id: u8, pub id: u8,
pub default_value: f64, pub default_value: f64,
pub name: &'static str,
} }
impl PartialEq for Attributes { impl PartialEq for Attributes {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
@ -61,6 +66,10 @@ pub fn build() -> TokenStream {
} }
impl Attributes { impl Attributes {
#(#constant_defs)* #(#constant_defs)*
pub const ALL: &'static [Self] = &[
#(Self::#constant_idents),*
];
} }
} }
} }

View File

@ -4,6 +4,7 @@ use std::hash::Hash;
pub struct Attributes { pub struct Attributes {
pub id: u8, pub id: u8,
pub default_value: f64, pub default_value: f64,
pub name: &'static str,
} }
impl PartialEq for Attributes { impl PartialEq for Attributes {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
@ -20,161 +21,243 @@ impl Attributes {
pub const AIR_DRAG_MODIFIER: Self = Self { pub const AIR_DRAG_MODIFIER: Self = Self {
id: 0, id: 0,
default_value: 1f64, default_value: 1f64,
name: "minecraft:air_drag_modifier",
}; };
pub const ARMOR: Self = Self { pub const ARMOR: Self = Self {
id: 1, id: 1,
default_value: 0f64, default_value: 0f64,
name: "minecraft:armor",
}; };
pub const ARMOR_TOUGHNESS: Self = Self { pub const ARMOR_TOUGHNESS: Self = Self {
id: 2, id: 2,
default_value: 0f64, default_value: 0f64,
name: "minecraft:armor_toughness",
}; };
pub const ATTACK_DAMAGE: Self = Self { pub const ATTACK_DAMAGE: Self = Self {
id: 3, id: 3,
default_value: 2f64, default_value: 2f64,
name: "minecraft:attack_damage",
}; };
pub const ATTACK_KNOCKBACK: Self = Self { pub const ATTACK_KNOCKBACK: Self = Self {
id: 4, id: 4,
default_value: 0f64, default_value: 0f64,
name: "minecraft:attack_knockback",
}; };
pub const ATTACK_SPEED: Self = Self { pub const ATTACK_SPEED: Self = Self {
id: 5, id: 5,
default_value: 4f64, default_value: 4f64,
name: "minecraft:attack_speed",
}; };
pub const BELOW_NAME_DISTANCE: Self = Self { pub const BELOW_NAME_DISTANCE: Self = Self {
id: 6, id: 6,
default_value: 10f64, default_value: 10f64,
name: "minecraft:below_name_distance",
}; };
pub const BLOCK_BREAK_SPEED: Self = Self { pub const BLOCK_BREAK_SPEED: Self = Self {
id: 7, id: 7,
default_value: 1f64, default_value: 1f64,
name: "minecraft:block_break_speed",
}; };
pub const BLOCK_INTERACTION_RANGE: Self = Self { pub const BLOCK_INTERACTION_RANGE: Self = Self {
id: 8, id: 8,
default_value: 4.5f64, default_value: 4.5f64,
name: "minecraft:block_interaction_range",
}; };
pub const BOUNCINESS: Self = Self { pub const BOUNCINESS: Self = Self {
id: 9, id: 9,
default_value: 0f64, default_value: 0f64,
name: "minecraft:bounciness",
}; };
pub const BURNING_TIME: Self = Self { pub const BURNING_TIME: Self = Self {
id: 10, id: 10,
default_value: 1f64, default_value: 1f64,
name: "minecraft:burning_time",
}; };
pub const CAMERA_DISTANCE: Self = Self { pub const CAMERA_DISTANCE: Self = Self {
id: 11, id: 11,
default_value: 4f64, default_value: 4f64,
name: "minecraft:camera_distance",
}; };
pub const EXPLOSION_KNOCKBACK_RESISTANCE: Self = Self { pub const EXPLOSION_KNOCKBACK_RESISTANCE: Self = Self {
id: 12, id: 12,
default_value: 0f64, default_value: 0f64,
name: "minecraft:explosion_knockback_resistance",
}; };
pub const ENTITY_INTERACTION_RANGE: Self = Self { pub const ENTITY_INTERACTION_RANGE: Self = Self {
id: 13, id: 13,
default_value: 3f64, default_value: 3f64,
name: "minecraft:entity_interaction_range",
}; };
pub const FALL_DAMAGE_MULTIPLIER: Self = Self { pub const FALL_DAMAGE_MULTIPLIER: Self = Self {
id: 14, id: 14,
default_value: 1f64, default_value: 1f64,
name: "minecraft:fall_damage_multiplier",
}; };
pub const FLYING_SPEED: Self = Self { pub const FLYING_SPEED: Self = Self {
id: 15, id: 15,
default_value: 0.4f64, default_value: 0.4f64,
name: "minecraft:flying_speed",
}; };
pub const FOLLOW_RANGE: Self = Self { pub const FOLLOW_RANGE: Self = Self {
id: 16, id: 16,
default_value: 32f64, default_value: 32f64,
name: "minecraft:follow_range",
}; };
pub const FRICTION_MODIFIER: Self = Self { pub const FRICTION_MODIFIER: Self = Self {
id: 17, id: 17,
default_value: 1f64, default_value: 1f64,
name: "minecraft:friction_modifier",
}; };
pub const GRAVITY: Self = Self { pub const GRAVITY: Self = Self {
id: 18, id: 18,
default_value: 0.08f64, default_value: 0.08f64,
name: "minecraft:gravity",
}; };
pub const JUMP_STRENGTH: Self = Self { pub const JUMP_STRENGTH: Self = Self {
id: 19, id: 19,
default_value: 0.41999998688697815f64, default_value: 0.41999998688697815f64,
name: "minecraft:jump_strength",
}; };
pub const KNOCKBACK_RESISTANCE: Self = Self { pub const KNOCKBACK_RESISTANCE: Self = Self {
id: 20, id: 20,
default_value: 0f64, default_value: 0f64,
name: "minecraft:knockback_resistance",
}; };
pub const LUCK: Self = Self { pub const LUCK: Self = Self {
id: 21, id: 21,
default_value: 0f64, default_value: 0f64,
name: "minecraft:luck",
}; };
pub const MAX_ABSORPTION: Self = Self { pub const MAX_ABSORPTION: Self = Self {
id: 22, id: 22,
default_value: 0f64, default_value: 0f64,
name: "minecraft:max_absorption",
}; };
pub const MAX_HEALTH: Self = Self { pub const MAX_HEALTH: Self = Self {
id: 23, id: 23,
default_value: 20f64, default_value: 20f64,
name: "minecraft:max_health",
}; };
pub const MINING_EFFICIENCY: Self = Self { pub const MINING_EFFICIENCY: Self = Self {
id: 24, id: 24,
default_value: 0f64, default_value: 0f64,
name: "minecraft:mining_efficiency",
}; };
pub const MOVEMENT_EFFICIENCY: Self = Self { pub const MOVEMENT_EFFICIENCY: Self = Self {
id: 25, id: 25,
default_value: 0f64, default_value: 0f64,
name: "minecraft:movement_efficiency",
}; };
pub const MOVEMENT_SPEED: Self = Self { pub const MOVEMENT_SPEED: Self = Self {
id: 26, id: 26,
default_value: 0.7f64, default_value: 0.7f64,
name: "minecraft:movement_speed",
}; };
pub const NAME_TAG_DISTANCE: Self = Self { pub const NAME_TAG_DISTANCE: Self = Self {
id: 27, id: 27,
default_value: 64f64, default_value: 64f64,
name: "minecraft:name_tag_distance",
}; };
pub const OXYGEN_BONUS: Self = Self { pub const OXYGEN_BONUS: Self = Self {
id: 28, id: 28,
default_value: 0f64, default_value: 0f64,
name: "minecraft:oxygen_bonus",
}; };
pub const SAFE_FALL_DISTANCE: Self = Self { pub const SAFE_FALL_DISTANCE: Self = Self {
id: 29, id: 29,
default_value: 3f64, default_value: 3f64,
name: "minecraft:safe_fall_distance",
}; };
pub const SCALE: Self = Self { pub const SCALE: Self = Self {
id: 30, id: 30,
default_value: 1f64, default_value: 1f64,
name: "minecraft:scale",
}; };
pub const SNEAKING_SPEED: Self = Self { pub const SNEAKING_SPEED: Self = Self {
id: 31, id: 31,
default_value: 0.3f64, default_value: 0.3f64,
name: "minecraft:sneaking_speed",
}; };
pub const SPAWN_REINFORCEMENTS: Self = Self { pub const SPAWN_REINFORCEMENTS: Self = Self {
id: 32, id: 32,
default_value: 0f64, default_value: 0f64,
name: "minecraft:spawn_reinforcements",
}; };
pub const STEP_HEIGHT: Self = Self { pub const STEP_HEIGHT: Self = Self {
id: 33, id: 33,
default_value: 0.6f64, default_value: 0.6f64,
name: "minecraft:step_height",
}; };
pub const SUBMERGED_MINING_SPEED: Self = Self { pub const SUBMERGED_MINING_SPEED: Self = Self {
id: 34, id: 34,
default_value: 0.2f64, default_value: 0.2f64,
name: "minecraft:submerged_mining_speed",
}; };
pub const SWEEPING_DAMAGE_RATIO: Self = Self { pub const SWEEPING_DAMAGE_RATIO: Self = Self {
id: 35, id: 35,
default_value: 0f64, default_value: 0f64,
name: "minecraft:sweeping_damage_ratio",
}; };
pub const TEMPT_RANGE: Self = Self { pub const TEMPT_RANGE: Self = Self {
id: 36, id: 36,
default_value: 10f64, default_value: 10f64,
name: "minecraft:tempt_range",
}; };
pub const WATER_MOVEMENT_EFFICIENCY: Self = Self { pub const WATER_MOVEMENT_EFFICIENCY: Self = Self {
id: 37, id: 37,
default_value: 0f64, default_value: 0f64,
name: "minecraft:water_movement_efficiency",
}; };
pub const WAYPOINT_TRANSMIT_RANGE: Self = Self { pub const WAYPOINT_TRANSMIT_RANGE: Self = Self {
id: 38, id: 38,
default_value: 0f64, default_value: 0f64,
name: "minecraft:waypoint_transmit_range",
}; };
pub const WAYPOINT_RECEIVE_RANGE: Self = Self { pub const WAYPOINT_RECEIVE_RANGE: Self = Self {
id: 39, id: 39,
default_value: 0f64, default_value: 0f64,
name: "minecraft:waypoint_receive_range",
}; };
pub const ALL: &'static [Self] = &[
Self::AIR_DRAG_MODIFIER,
Self::ARMOR,
Self::ARMOR_TOUGHNESS,
Self::ATTACK_DAMAGE,
Self::ATTACK_KNOCKBACK,
Self::ATTACK_SPEED,
Self::BELOW_NAME_DISTANCE,
Self::BLOCK_BREAK_SPEED,
Self::BLOCK_INTERACTION_RANGE,
Self::BOUNCINESS,
Self::BURNING_TIME,
Self::CAMERA_DISTANCE,
Self::EXPLOSION_KNOCKBACK_RESISTANCE,
Self::ENTITY_INTERACTION_RANGE,
Self::FALL_DAMAGE_MULTIPLIER,
Self::FLYING_SPEED,
Self::FOLLOW_RANGE,
Self::FRICTION_MODIFIER,
Self::GRAVITY,
Self::JUMP_STRENGTH,
Self::KNOCKBACK_RESISTANCE,
Self::LUCK,
Self::MAX_ABSORPTION,
Self::MAX_HEALTH,
Self::MINING_EFFICIENCY,
Self::MOVEMENT_EFFICIENCY,
Self::MOVEMENT_SPEED,
Self::NAME_TAG_DISTANCE,
Self::OXYGEN_BONUS,
Self::SAFE_FALL_DISTANCE,
Self::SCALE,
Self::SNEAKING_SPEED,
Self::SPAWN_REINFORCEMENTS,
Self::STEP_HEIGHT,
Self::SUBMERGED_MINING_SPEED,
Self::SWEEPING_DAMAGE_RATIO,
Self::TEMPT_RANGE,
Self::WATER_MOVEMENT_EFFICIENCY,
Self::WAYPOINT_TRANSMIT_RANGE,
Self::WAYPOINT_RECEIVE_RANGE,
];
} }

View File

@ -0,0 +1,66 @@
use std::pin::Pin;
use crate::command::{
argument_types::FromStringReader,
argument_types::argument_type::{ArgumentType, JavaClientArgumentType},
context::command_context::CommandContext,
errors::command_syntax_error::CommandSyntaxError,
errors::error_types::CommandErrorType,
string_reader::StringReader,
suggestion::suggestions::{Suggestions, SuggestionsBuilder},
};
use pumpkin_data::attributes::Attributes;
use pumpkin_data::translation;
use pumpkin_util::identifier::Identifier;
use pumpkin_util::text::TextComponent;
pub const INVALID_ATTRIBUTE_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new(
translation::java::ARGUMENT_ID_INVALID,
translation::java::ARGUMENT_ID_INVALID,
);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct AttributeArgumentType;
impl ArgumentType for AttributeArgumentType {
type Item = Attributes;
fn parse(&self, reader: &mut StringReader) -> Result<Self::Item, CommandSyntaxError> {
let identifier = Identifier::from_reader(reader)?;
let ident_str = identifier.to_string();
let attr = Attributes::ALL
.iter()
.find(|a| a.name == ident_str)
.cloned()
.ok_or_else(|| {
INVALID_ATTRIBUTE_ERROR_TYPE.create(reader, TextComponent::text(ident_str))
})?;
Ok(attr)
}
fn client_side_parser(&'_ self) -> JavaClientArgumentType {
JavaClientArgumentType::Resource {
identifier: Identifier::vanilla("attribute").unwrap(),
}
}
fn list_suggestions<'a>(
&'a self,
_context: &'a CommandContext,
builder: SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + Send + 'a>> {
Box::pin(async move {
builder
.filter_and_suggest_iter(Attributes::ALL.iter().map(|a| a.name))
.build()
})
}
}
impl AttributeArgumentType {
pub fn get(context: &CommandContext, name: &str) -> Result<Attributes, CommandSyntaxError> {
context.get_argument::<Attributes>(name).cloned()
}
}

View File

@ -217,6 +217,7 @@ impl FromStringReader for Identifier {
} }
pub mod argument_type; pub mod argument_type;
pub mod attribute;
pub mod block; pub mod block;
pub mod coordinates; pub mod coordinates;
pub mod core; pub mod core;

View File

@ -0,0 +1,531 @@
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
use crate::command::argument_types::attribute::AttributeArgumentType;
use crate::command::argument_types::core::double::DoubleArgumentType;
use crate::command::argument_types::core::string::StringArgumentType;
use crate::command::argument_types::entity::EntityArgumentType;
use crate::command::argument_types::uuid::UuidArgumentType;
use crate::command::context::command_context::CommandContext;
use crate::command::errors::error_types::CommandErrorType;
use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult};
use crate::entity::attributes::ModifierOperation;
use pumpkin_data::attributes::Attributes;
use pumpkin_data::translation;
use pumpkin_util::PermissionLvl;
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
use pumpkin_util::text::TextComponent;
const DESCRIPTION: &str = "Queries or modifies attributes of an entity.";
const PERMISSION: &str = "minecraft:command.attribute";
const ARG_TARGET: &str = "target";
const ARG_ATTRIBUTE: &str = "attribute";
const ARG_SCALE: &str = "scale";
const ARG_VALUE: &str = "value";
const ARG_UUID: &str = "uuid";
const ARG_NAME: &str = "name";
const FAILED_ENTITY_ERROR: CommandErrorType<1> = CommandErrorType::new(
translation::java::COMMANDS_ATTRIBUTE_FAILED_ENTITY,
translation::java::COMMANDS_ATTRIBUTE_FAILED_ENTITY,
);
const NO_ATTRIBUTE_ERROR: CommandErrorType<2> = CommandErrorType::new(
translation::java::COMMANDS_ATTRIBUTE_FAILED_NO_ATTRIBUTE,
translation::java::COMMANDS_ATTRIBUTE_FAILED_NO_ATTRIBUTE,
);
const MODIFIER_ALREADY_PRESENT_ERROR: CommandErrorType<3> = CommandErrorType::new(
translation::java::COMMANDS_ATTRIBUTE_FAILED_MODIFIER_ALREADY_PRESENT,
translation::java::COMMANDS_ATTRIBUTE_FAILED_MODIFIER_ALREADY_PRESENT,
);
const NO_MODIFIER_ERROR: CommandErrorType<3> = CommandErrorType::new(
translation::java::COMMANDS_ATTRIBUTE_FAILED_NO_MODIFIER,
translation::java::COMMANDS_ATTRIBUTE_FAILED_NO_MODIFIER,
);
fn attribute_translation_key(attribute: &Attributes) -> String {
let path = attribute
.name
.strip_prefix("minecraft:")
.unwrap_or(attribute.name);
format!("attribute.name.{path}")
}
struct GetExecutor {
is_base: bool,
has_scale: bool,
}
impl CommandExecutor for GetExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let target = EntityArgumentType::get_entity(context, ARG_TARGET).await?;
let living = target
.get_living_entity()
.ok_or_else(|| FAILED_ENTITY_ERROR.create_without_context(target.get_name()))?;
let attribute = AttributeArgumentType::get(context, ARG_ATTRIBUTE)?;
let scale = if self.has_scale {
DoubleArgumentType::get(context, ARG_SCALE)?
} else {
1.0
};
let has_attr = living
.attributes
.read()
.unwrap()
.contains_key(&attribute.id);
if !has_attr {
return Err(NO_ATTRIBUTE_ERROR.create_without_context(
target.get_name(),
TextComponent::translate(attribute_translation_key(&attribute), []),
));
}
if self.is_base {
let base_value = living.get_attribute_base(&attribute);
let scaled_value = base_value * scale;
context
.source
.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_ATTRIBUTE_BASE_VALUE_GET_SUCCESS,
translation::java::COMMANDS_ATTRIBUTE_BASE_VALUE_GET_SUCCESS,
[
TextComponent::translate(attribute_translation_key(&attribute), []),
target.get_name(),
TextComponent::text(base_value.to_string()),
],
),
false,
)
.await;
Ok(scaled_value as i32)
} else {
let value = living.get_attribute_value(&attribute);
let scaled_value = value * scale;
context
.source
.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_ATTRIBUTE_VALUE_GET_SUCCESS,
translation::java::COMMANDS_ATTRIBUTE_VALUE_GET_SUCCESS,
[
TextComponent::translate(attribute_translation_key(&attribute), []),
target.get_name(),
TextComponent::text(value.to_string()),
],
),
false,
)
.await;
Ok(scaled_value as i32)
}
})
}
}
struct BaseSetExecutor;
impl CommandExecutor for BaseSetExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let target = EntityArgumentType::get_entity(context, ARG_TARGET).await?;
let living = target
.get_living_entity()
.ok_or_else(|| FAILED_ENTITY_ERROR.create_without_context(target.get_name()))?;
let attribute = AttributeArgumentType::get(context, ARG_ATTRIBUTE)?;
let value = DoubleArgumentType::get(context, ARG_VALUE)?;
let has_attr = living
.attributes
.read()
.unwrap()
.contains_key(&attribute.id);
if !has_attr {
return Err(NO_ATTRIBUTE_ERROR.create_without_context(
target.get_name(),
TextComponent::translate(attribute_translation_key(&attribute), []),
));
}
living.set_attribute_base(&attribute, value);
crate::entity::attributes::send_attribute_updates_for_living(
living,
vec![attribute.clone()],
)
.await;
context
.source
.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_ATTRIBUTE_BASE_VALUE_SET_SUCCESS,
translation::java::COMMANDS_ATTRIBUTE_BASE_VALUE_SET_SUCCESS,
[
TextComponent::translate(attribute_translation_key(&attribute), []),
target.get_name(),
TextComponent::text(value.to_string()),
],
),
true,
)
.await;
Ok(value as i32)
})
}
}
struct BaseResetExecutor;
impl CommandExecutor for BaseResetExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let target = EntityArgumentType::get_entity(context, ARG_TARGET).await?;
let living = target
.get_living_entity()
.ok_or_else(|| FAILED_ENTITY_ERROR.create_without_context(target.get_name()))?;
let attribute = AttributeArgumentType::get(context, ARG_ATTRIBUTE)?;
let has_attr = living
.attributes
.read()
.unwrap()
.contains_key(&attribute.id);
if !has_attr {
return Err(NO_ATTRIBUTE_ERROR.create_without_context(
target.get_name(),
TextComponent::translate(attribute_translation_key(&attribute), []),
));
}
let default_val = attribute.default_value;
living.set_attribute_base(&attribute, default_val);
crate::entity::attributes::send_attribute_updates_for_living(
living,
vec![attribute.clone()],
)
.await;
context
.source
.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_ATTRIBUTE_BASE_VALUE_RESET_SUCCESS,
translation::java::COMMANDS_ATTRIBUTE_BASE_VALUE_RESET_SUCCESS,
[
TextComponent::translate(attribute_translation_key(&attribute), []),
target.get_name(),
TextComponent::text(default_val.to_string()),
],
),
true,
)
.await;
Ok(default_val as i32)
})
}
}
struct ModifierAddExecutor {
operation: ModifierOperation,
}
impl CommandExecutor for ModifierAddExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let target = EntityArgumentType::get_entity(context, ARG_TARGET).await?;
let living = target
.get_living_entity()
.ok_or_else(|| FAILED_ENTITY_ERROR.create_without_context(target.get_name()))?;
let attribute = AttributeArgumentType::get(context, ARG_ATTRIBUTE)?;
let uuid = UuidArgumentType::get(context, ARG_UUID)?;
let _name = StringArgumentType::get(context, ARG_NAME)?;
let value = DoubleArgumentType::get(context, ARG_VALUE)?;
let modifier_id = uuid.to_string();
let res = {
let mut map = living.attributes.write().unwrap();
let inst = map.get_mut(&attribute.id).ok_or_else(|| {
NO_ATTRIBUTE_ERROR.create_without_context(
target.get_name(),
TextComponent::translate(attribute_translation_key(&attribute), []),
)
})?;
if inst.modifiers.iter().any(|m| m.id == modifier_id) {
Err(MODIFIER_ALREADY_PRESENT_ERROR.create_without_context(
TextComponent::text(modifier_id.clone()),
TextComponent::translate(attribute_translation_key(&attribute), []),
target.get_name(),
))
} else {
inst.modifiers.push(crate::entity::attributes::Modifier {
id: modifier_id.clone(),
amount: value,
operation: self.operation,
});
inst.dirty.store(true, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
};
res?;
crate::entity::attributes::send_attribute_updates_for_living(
living,
vec![attribute.clone()],
)
.await;
context
.source
.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_ATTRIBUTE_MODIFIER_ADD_SUCCESS,
translation::java::COMMANDS_ATTRIBUTE_MODIFIER_ADD_SUCCESS,
[
TextComponent::text(modifier_id),
TextComponent::translate(attribute_translation_key(&attribute), []),
target.get_name(),
],
),
true,
)
.await;
Ok(1)
})
}
}
struct ModifierRemoveExecutor;
impl CommandExecutor for ModifierRemoveExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let target = EntityArgumentType::get_entity(context, ARG_TARGET).await?;
let living = target
.get_living_entity()
.ok_or_else(|| FAILED_ENTITY_ERROR.create_without_context(target.get_name()))?;
let attribute = AttributeArgumentType::get(context, ARG_ATTRIBUTE)?;
let uuid = UuidArgumentType::get(context, ARG_UUID)?;
let modifier_id = uuid.to_string();
let res = {
let mut map = living.attributes.write().unwrap();
let inst = map.get_mut(&attribute.id).ok_or_else(|| {
NO_ATTRIBUTE_ERROR.create_without_context(
target.get_name(),
TextComponent::translate(attribute_translation_key(&attribute), []),
)
})?;
let index = inst
.modifiers
.iter()
.position(|m| m.id == modifier_id)
.ok_or_else(|| {
NO_MODIFIER_ERROR.create_without_context(
TextComponent::translate(attribute_translation_key(&attribute), []),
target.get_name(),
TextComponent::text(modifier_id.clone()),
)
})?;
inst.modifiers.remove(index);
inst.dirty.store(true, std::sync::atomic::Ordering::Relaxed);
Ok(())
};
res?;
crate::entity::attributes::send_attribute_updates_for_living(
living,
vec![attribute.clone()],
)
.await;
context
.source
.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_ATTRIBUTE_MODIFIER_REMOVE_SUCCESS,
translation::java::COMMANDS_ATTRIBUTE_MODIFIER_REMOVE_SUCCESS,
[
TextComponent::text(modifier_id),
TextComponent::translate(attribute_translation_key(&attribute), []),
target.get_name(),
],
),
true,
)
.await;
Ok(1)
})
}
}
struct ModifierGetExecutor;
impl CommandExecutor for ModifierGetExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let target = EntityArgumentType::get_entity(context, ARG_TARGET).await?;
let living = target
.get_living_entity()
.ok_or_else(|| FAILED_ENTITY_ERROR.create_without_context(target.get_name()))?;
let attribute = AttributeArgumentType::get(context, ARG_ATTRIBUTE)?;
let uuid = UuidArgumentType::get(context, ARG_UUID)?;
let modifier_id = uuid.to_string();
let val = {
let map = living.attributes.read().unwrap();
let inst = map.get(&attribute.id).ok_or_else(|| {
NO_ATTRIBUTE_ERROR.create_without_context(
target.get_name(),
TextComponent::translate(attribute_translation_key(&attribute), []),
)
})?;
let modifier = inst
.modifiers
.iter()
.find(|m| m.id == modifier_id)
.ok_or_else(|| {
NO_MODIFIER_ERROR.create_without_context(
TextComponent::translate(attribute_translation_key(&attribute), []),
target.get_name(),
TextComponent::text(modifier_id.clone()),
)
})?;
modifier.amount
};
context
.source
.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_ATTRIBUTE_MODIFIER_VALUE_GET_SUCCESS,
translation::java::COMMANDS_ATTRIBUTE_MODIFIER_VALUE_GET_SUCCESS,
[
TextComponent::text(modifier_id),
TextComponent::translate(attribute_translation_key(&attribute), []),
target.get_name(),
TextComponent::text(val.to_string()),
],
),
false,
)
.await;
Ok(val as i32)
})
}
}
#[expect(clippy::too_many_lines)]
pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) {
registry.register_permission_or_panic(Permission::new(
PERMISSION,
DESCRIPTION,
PermissionDefault::Op(PermissionLvl::Two),
));
dispatcher.register(
command("attribute", DESCRIPTION).requires(PERMISSION).then(
argument(ARG_TARGET, EntityArgumentType::Entity).then(
argument(ARG_ATTRIBUTE, AttributeArgumentType)
.then(
literal("get")
.executes(GetExecutor {
is_base: false,
has_scale: false,
})
.then(argument(ARG_SCALE, DoubleArgumentType::any()).executes(
GetExecutor {
is_base: false,
has_scale: true,
},
)),
)
.then(
literal("base")
.then(
literal("get")
.executes(GetExecutor {
is_base: true,
has_scale: false,
})
.then(argument(ARG_SCALE, DoubleArgumentType::any()).executes(
GetExecutor {
is_base: true,
has_scale: true,
},
)),
)
.then(
literal("set").then(
argument(ARG_VALUE, DoubleArgumentType::any())
.executes(BaseSetExecutor),
),
)
.then(literal("reset").executes(BaseResetExecutor)),
)
.then(
literal("modifier")
.then(
literal("add").then(
argument(ARG_UUID, UuidArgumentType).then(
argument(ARG_NAME, StringArgumentType::SingleWord).then(
argument(ARG_VALUE, DoubleArgumentType::any())
.then(literal("add").executes(
ModifierAddExecutor {
operation: ModifierOperation::Add,
},
))
.then(literal("multiply").executes(
ModifierAddExecutor {
operation: ModifierOperation::MultiplyTotal,
},
))
.then(literal("multiply_base").executes(
ModifierAddExecutor {
operation: ModifierOperation::MultiplyBase,
},
)),
),
),
),
)
.then(
literal("remove").then(
argument(ARG_UUID, UuidArgumentType)
.executes(ModifierRemoveExecutor),
),
)
.then(literal("value").then(literal("get").then(
argument(ARG_UUID, UuidArgumentType).executes(ModifierGetExecutor),
))),
),
),
),
);
}

View File

@ -7,6 +7,7 @@ use pumpkin_util::{
use tokio::sync::RwLock; use tokio::sync::RwLock;
mod advancement; mod advancement;
mod attribute;
mod ban; mod ban;
mod banip; mod banip;
mod banlist; mod banlist;
@ -175,6 +176,7 @@ pub async fn default_dispatcher(
trigger::register(&mut dispatcher, registry); trigger::register(&mut dispatcher, registry);
scoreboard::register(&mut dispatcher, registry); scoreboard::register(&mut dispatcher, registry);
clone::register(&mut dispatcher, registry); clone::register(&mut dispatcher, registry);
attribute::register(&mut dispatcher, registry);
dispatcher dispatcher
} }