From 486f8d2cfeeb3776ca604a294b74d8ac5d930bd0 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Mon, 13 Jul 2026 18:43:45 +0200 Subject: [PATCH] feat(command): add attributes --- pumpkin-codegen/Cargo.lock | 1 + pumpkin-codegen/src/attributes.rs | 9 + pumpkin-data/src/generated/attributes.rs | 83 +++ .../src/command/argument_types/attribute.rs | 66 +++ pumpkin/src/command/argument_types/mod.rs | 1 + pumpkin/src/command/commands/attribute.rs | 531 ++++++++++++++++++ pumpkin/src/command/commands/mod.rs | 2 + 7 files changed, 693 insertions(+) create mode 100644 pumpkin/src/command/argument_types/attribute.rs create mode 100644 pumpkin/src/command/commands/attribute.rs diff --git a/pumpkin-codegen/Cargo.lock b/pumpkin-codegen/Cargo.lock index 5bffe522d..a1078f00d 100644 --- a/pumpkin-codegen/Cargo.lock +++ b/pumpkin-codegen/Cargo.lock @@ -803,6 +803,7 @@ dependencies = [ "serde", "thiserror", "tracing", + "uuid", ] [[package]] diff --git a/pumpkin-codegen/src/attributes.rs b/pumpkin-codegen/src/attributes.rs index f2b75b29a..513df07aa 100644 --- a/pumpkin-codegen/src/attributes.rs +++ b/pumpkin-codegen/src/attributes.rs @@ -25,17 +25,21 @@ pub fn build() -> TokenStream { sorted_attributes.sort_by_key(|(_, raw)| raw.id); let mut constant_defs = Vec::new(); + let mut constant_idents = Vec::new(); for (raw_name, raw_value) in sorted_attributes { 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 default_value_lit = raw_value.default_value; + let name_str = format!("minecraft:{raw_name}"); constant_defs.push(quote!( pub const #constant_ident: Self = Self { id: #id_lit, default_value: #default_value_lit, + name: #name_str, }; )); } @@ -47,6 +51,7 @@ pub fn build() -> TokenStream { pub struct Attributes { pub id: u8, pub default_value: f64, + pub name: &'static str, } impl PartialEq for Attributes { fn eq(&self, other: &Self) -> bool { @@ -61,6 +66,10 @@ pub fn build() -> TokenStream { } impl Attributes { #(#constant_defs)* + + pub const ALL: &'static [Self] = &[ + #(Self::#constant_idents),* + ]; } } } diff --git a/pumpkin-data/src/generated/attributes.rs b/pumpkin-data/src/generated/attributes.rs index 85c90f38b..ce7d9330f 100644 --- a/pumpkin-data/src/generated/attributes.rs +++ b/pumpkin-data/src/generated/attributes.rs @@ -4,6 +4,7 @@ use std::hash::Hash; pub struct Attributes { pub id: u8, pub default_value: f64, + pub name: &'static str, } impl PartialEq for Attributes { fn eq(&self, other: &Self) -> bool { @@ -20,161 +21,243 @@ impl Attributes { pub const AIR_DRAG_MODIFIER: Self = Self { id: 0, default_value: 1f64, + name: "minecraft:air_drag_modifier", }; pub const ARMOR: Self = Self { id: 1, default_value: 0f64, + name: "minecraft:armor", }; pub const ARMOR_TOUGHNESS: Self = Self { id: 2, default_value: 0f64, + name: "minecraft:armor_toughness", }; pub const ATTACK_DAMAGE: Self = Self { id: 3, default_value: 2f64, + name: "minecraft:attack_damage", }; pub const ATTACK_KNOCKBACK: Self = Self { id: 4, default_value: 0f64, + name: "minecraft:attack_knockback", }; pub const ATTACK_SPEED: Self = Self { id: 5, default_value: 4f64, + name: "minecraft:attack_speed", }; pub const BELOW_NAME_DISTANCE: Self = Self { id: 6, default_value: 10f64, + name: "minecraft:below_name_distance", }; pub const BLOCK_BREAK_SPEED: Self = Self { id: 7, default_value: 1f64, + name: "minecraft:block_break_speed", }; pub const BLOCK_INTERACTION_RANGE: Self = Self { id: 8, default_value: 4.5f64, + name: "minecraft:block_interaction_range", }; pub const BOUNCINESS: Self = Self { id: 9, default_value: 0f64, + name: "minecraft:bounciness", }; pub const BURNING_TIME: Self = Self { id: 10, default_value: 1f64, + name: "minecraft:burning_time", }; pub const CAMERA_DISTANCE: Self = Self { id: 11, default_value: 4f64, + name: "minecraft:camera_distance", }; pub const EXPLOSION_KNOCKBACK_RESISTANCE: Self = Self { id: 12, default_value: 0f64, + name: "minecraft:explosion_knockback_resistance", }; pub const ENTITY_INTERACTION_RANGE: Self = Self { id: 13, default_value: 3f64, + name: "minecraft:entity_interaction_range", }; pub const FALL_DAMAGE_MULTIPLIER: Self = Self { id: 14, default_value: 1f64, + name: "minecraft:fall_damage_multiplier", }; pub const FLYING_SPEED: Self = Self { id: 15, default_value: 0.4f64, + name: "minecraft:flying_speed", }; pub const FOLLOW_RANGE: Self = Self { id: 16, default_value: 32f64, + name: "minecraft:follow_range", }; pub const FRICTION_MODIFIER: Self = Self { id: 17, default_value: 1f64, + name: "minecraft:friction_modifier", }; pub const GRAVITY: Self = Self { id: 18, default_value: 0.08f64, + name: "minecraft:gravity", }; pub const JUMP_STRENGTH: Self = Self { id: 19, default_value: 0.41999998688697815f64, + name: "minecraft:jump_strength", }; pub const KNOCKBACK_RESISTANCE: Self = Self { id: 20, default_value: 0f64, + name: "minecraft:knockback_resistance", }; pub const LUCK: Self = Self { id: 21, default_value: 0f64, + name: "minecraft:luck", }; pub const MAX_ABSORPTION: Self = Self { id: 22, default_value: 0f64, + name: "minecraft:max_absorption", }; pub const MAX_HEALTH: Self = Self { id: 23, default_value: 20f64, + name: "minecraft:max_health", }; pub const MINING_EFFICIENCY: Self = Self { id: 24, default_value: 0f64, + name: "minecraft:mining_efficiency", }; pub const MOVEMENT_EFFICIENCY: Self = Self { id: 25, default_value: 0f64, + name: "minecraft:movement_efficiency", }; pub const MOVEMENT_SPEED: Self = Self { id: 26, default_value: 0.7f64, + name: "minecraft:movement_speed", }; pub const NAME_TAG_DISTANCE: Self = Self { id: 27, default_value: 64f64, + name: "minecraft:name_tag_distance", }; pub const OXYGEN_BONUS: Self = Self { id: 28, default_value: 0f64, + name: "minecraft:oxygen_bonus", }; pub const SAFE_FALL_DISTANCE: Self = Self { id: 29, default_value: 3f64, + name: "minecraft:safe_fall_distance", }; pub const SCALE: Self = Self { id: 30, default_value: 1f64, + name: "minecraft:scale", }; pub const SNEAKING_SPEED: Self = Self { id: 31, default_value: 0.3f64, + name: "minecraft:sneaking_speed", }; pub const SPAWN_REINFORCEMENTS: Self = Self { id: 32, default_value: 0f64, + name: "minecraft:spawn_reinforcements", }; pub const STEP_HEIGHT: Self = Self { id: 33, default_value: 0.6f64, + name: "minecraft:step_height", }; pub const SUBMERGED_MINING_SPEED: Self = Self { id: 34, default_value: 0.2f64, + name: "minecraft:submerged_mining_speed", }; pub const SWEEPING_DAMAGE_RATIO: Self = Self { id: 35, default_value: 0f64, + name: "minecraft:sweeping_damage_ratio", }; pub const TEMPT_RANGE: Self = Self { id: 36, default_value: 10f64, + name: "minecraft:tempt_range", }; pub const WATER_MOVEMENT_EFFICIENCY: Self = Self { id: 37, default_value: 0f64, + name: "minecraft:water_movement_efficiency", }; pub const WAYPOINT_TRANSMIT_RANGE: Self = Self { id: 38, default_value: 0f64, + name: "minecraft:waypoint_transmit_range", }; pub const WAYPOINT_RECEIVE_RANGE: Self = Self { id: 39, 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, + ]; } diff --git a/pumpkin/src/command/argument_types/attribute.rs b/pumpkin/src/command/argument_types/attribute.rs new file mode 100644 index 000000000..dc9181395 --- /dev/null +++ b/pumpkin/src/command/argument_types/attribute.rs @@ -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 { + 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 + 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 { + context.get_argument::(name).cloned() + } +} diff --git a/pumpkin/src/command/argument_types/mod.rs b/pumpkin/src/command/argument_types/mod.rs index 9e2bad2bd..d39cd104a 100644 --- a/pumpkin/src/command/argument_types/mod.rs +++ b/pumpkin/src/command/argument_types/mod.rs @@ -217,6 +217,7 @@ impl FromStringReader for Identifier { } pub mod argument_type; +pub mod attribute; pub mod block; pub mod coordinates; pub mod core; diff --git a/pumpkin/src/command/commands/attribute.rs b/pumpkin/src/command/commands/attribute.rs new file mode 100644 index 000000000..c5342c24a --- /dev/null +++ b/pumpkin/src/command/commands/attribute.rs @@ -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), + ))), + ), + ), + ), + ); +} diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index af7e23050..c5e69fc56 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -7,6 +7,7 @@ use pumpkin_util::{ use tokio::sync::RwLock; mod advancement; +mod attribute; mod ban; mod banip; mod banlist; @@ -175,6 +176,7 @@ pub async fn default_dispatcher( trigger::register(&mut dispatcher, registry); scoreboard::register(&mut dispatcher, registry); clone::register(&mut dispatcher, registry); + attribute::register(&mut dispatcher, registry); dispatcher }