use std::sync::Arc; use std::sync::atomic::Ordering; use pumpkin_util::math::experience; use pumpkin_util::text::TextComponent; use crate::command::args::bounded_num::BoundedNumArgumentConsumer; use crate::command::args::players::PlayersArgumentConsumer; use crate::command::args::{ConsumedArgs, FindArg}; use crate::command::dispatcher::CommandError; use crate::command::tree::CommandTree; use crate::command::tree::builder::{argument, literal}; use crate::command::{CommandExecutor, CommandResult, CommandSender}; use crate::entity::EntityBase; use crate::entity::player::Player; const NAMES: [&str; 2] = ["experience", "xp"]; const DESCRIPTION: &str = "Add, set or query player experience."; const ARG_TARGETS: &str = "targets"; const ARG_AMOUNT: &str = "amount"; const fn xp_amount() -> BoundedNumArgumentConsumer { BoundedNumArgumentConsumer::new() .name(ARG_AMOUNT) .min(0) .max(i32::MAX) } #[derive(Clone, Copy, PartialEq)] enum Mode { Add, Set, Query, } #[derive(Clone, Copy, PartialEq)] enum ExpType { Points, Levels, } struct Executor { mode: Mode, exp_type: ExpType, } impl Executor { async fn handle_query(&self, sender: &CommandSender, target: &Player) -> i32 { let (val, translation_key) = match self.exp_type { ExpType::Levels => ( target.experience_level.load(Ordering::Relaxed), "commands.experience.query.levels", ), ExpType::Points => ( target.experience_points.load(Ordering::Relaxed), "commands.experience.query.points", ), }; sender .send_message(TextComponent::translate_cross( translation_key, translation_key, [ target.get_display_name(), TextComponent::text(val.to_string()), ], )) .await; val } fn get_success_message( &self, amount: i32, targets: &[Arc], first_target_name: TextComponent, ) -> TextComponent { let type_str = match self.exp_type { ExpType::Points => "points", ExpType::Levels => "levels", }; let mode_str = match self.mode { Mode::Add => "add", Mode::Set => "set", Mode::Query => "query", }; let bedrock_key = match self.exp_type { ExpType::Points => pumpkin_data::translation::bedrock::COMMANDS_XP_SUCCESS, ExpType::Levels => { if amount >= 0 { pumpkin_data::translation::bedrock::COMMANDS_XP_SUCCESS_LEVELS } else { pumpkin_data::translation::bedrock::COMMANDS_XP_SUCCESS_NEGATIVE_LEVELS } } }; let bedrock_amount = if amount >= 0 { amount } else { amount.abs() }; if targets.len() > 1 { TextComponent::translate_cross( format!("commands.experience.{mode_str}.{type_str}.success.multiple"), bedrock_key, [ TextComponent::text(bedrock_amount.to_string()), TextComponent::text(targets.len().to_string()), ], ) } else { TextComponent::translate_cross( format!("commands.experience.{mode_str}.{type_str}.success.single"), bedrock_key, [ TextComponent::text(bedrock_amount.to_string()), first_target_name, ], ) } } /// Returns `true` if successful. Otherwise, there was a problem setting the points of a player. async fn handle_modify(&self, target: &Arc, amount: i32) -> bool { match self.exp_type { ExpType::Levels => { if self.mode == Mode::Add { target.add_experience_levels(amount); } else { target.set_experience_level(amount, true); } } ExpType::Points => { if self.mode == Mode::Add { target.add_experience_points(amount); } else { let current_lvl = target.experience_level.load(Ordering::Relaxed); if amount > experience::points_in_level(current_lvl) { return false; } target.set_experience_points(amount); } } } true } } impl CommandExecutor for Executor { fn execute<'a>( &'a self, sender: &'a CommandSender, _server: &'a crate::server::Server, args: &'a ConsumedArgs<'a>, ) -> CommandResult<'a> { Box::pin(async move { let targets = PlayersArgumentConsumer::find_arg(args, ARG_TARGETS)?; if self.mode == Mode::Query { let target = targets.first().ok_or_else(|| { CommandError::CommandFailed(TextComponent::translate_cross( "argument.player.unknown", "argument.player.unknown", [], )) })?; if targets.len() > 1 { return Err(CommandError::CommandFailed(TextComponent::translate_cross( "argument.player.toomany", "argument.player.toomany", [], ))); } return Ok(self.handle_query(sender, target).await); } // Handle Add/Set let amount = BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT)??; let mut successes = 0; for target in targets { if self.handle_modify(target, amount).await { successes += 1; } } if successes == 0 { return Err(CommandError::CommandFailed(TextComponent::translate_cross( "commands.experience.set.points.invalid", "commands.experience.set.points.invalid", [], ))); } // Safe to access first() because successes > 0 let first_name = targets[0].get_display_name(); let msg = self.get_success_message(amount, targets, first_name); sender.send_message(msg).await; Ok(successes) }) } } pub fn init_command_tree() -> CommandTree { CommandTree::new(NAMES, DESCRIPTION) .then( literal("add").then( argument(ARG_TARGETS, PlayersArgumentConsumer).then( argument(ARG_AMOUNT, xp_amount()) .then(literal("levels").execute(Executor { mode: Mode::Add, exp_type: ExpType::Levels, })) .then(literal("points").execute(Executor { mode: Mode::Add, exp_type: ExpType::Points, })) .execute(Executor { mode: Mode::Add, exp_type: ExpType::Points, }), ), ), ) .then( literal("set").then( argument(ARG_TARGETS, PlayersArgumentConsumer).then( argument(ARG_AMOUNT, xp_amount()) .then(literal("levels").execute(Executor { mode: Mode::Set, exp_type: ExpType::Levels, })) .then(literal("points").execute(Executor { mode: Mode::Set, exp_type: ExpType::Points, })) .execute(Executor { mode: Mode::Set, exp_type: ExpType::Points, }), ), ), ) .then( literal("query").then( argument(ARG_TARGETS, PlayersArgumentConsumer) .then(literal("levels").execute(Executor { mode: Mode::Query, exp_type: ExpType::Levels, })) .then(literal("points").execute(Executor { mode: Mode::Query, exp_type: ExpType::Points, })), ), ) }