diff --git a/pumpkin/src/command/argument_types/mod.rs b/pumpkin/src/command/argument_types/mod.rs index 3a66b05cb..55b1a358a 100644 --- a/pumpkin/src/command/argument_types/mod.rs +++ b/pumpkin/src/command/argument_types/mod.rs @@ -226,6 +226,7 @@ pub mod game_profile; pub mod hex_color; pub mod identifier; pub mod nbt; +pub mod objective; pub mod range; pub mod resource_key; pub mod slot; diff --git a/pumpkin/src/command/argument_types/objective.rs b/pumpkin/src/command/argument_types/objective.rs new file mode 100644 index 000000000..068147d69 --- /dev/null +++ b/pumpkin/src/command/argument_types/objective.rs @@ -0,0 +1,50 @@ +use crate::command::{ + argument_types::argument_type::{ArgumentType, JavaClientArgumentType}, + context::command_context::CommandContext, + errors::command_syntax_error::CommandSyntaxError, + string_reader::StringReader, + suggestion::suggestions::{Suggestions, SuggestionsBuilder}, +}; +use std::pin::Pin; + +/// Represents an argument type parsing a scoreboard objective name. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct ObjectiveArgumentType; + +impl ArgumentType for ObjectiveArgumentType { + type Item = String; + + fn parse(&self, reader: &mut StringReader) -> Result { + let name = reader.read_unquoted_string(); + Ok(name) + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::Objective + } + + fn list_suggestions<'a>( + &'a self, + context: &'a CommandContext, + mut builder: SuggestionsBuilder, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let scoreboard = context.world().scoreboard.lock().await; + for objective_name in scoreboard.get_objectives().keys() { + builder = builder.filter_and_suggest_one(objective_name.as_str()); + } + builder.build() + }) + } + + fn examples(&self) -> Vec { + vec!["objective".to_string(), "trigger_obj".to_string()] + } +} + +impl ObjectiveArgumentType { + /// Returns a [`CommandContext`]'s parsed `String` argument as a string slice. + pub fn get<'a>(context: &'a CommandContext, name: &str) -> Result<&'a str, CommandSyntaxError> { + Ok(context.get_argument::(name)?.as_str()) + } +} diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 4deb0a7e3..a5c84ead7 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -44,6 +44,7 @@ mod saveall; mod saveoff; mod saveon; mod say; +mod scoreboard; mod seed; mod setblock; mod setidletimeout; @@ -61,6 +62,7 @@ mod time; mod title; mod tps; mod transfer; +mod trigger; mod weather; mod whitelist; mod worldborder; @@ -169,6 +171,8 @@ pub async fn default_dispatcher( tag::register(&mut dispatcher, registry); tick::register(&mut dispatcher, registry); advancement::register(&mut dispatcher, registry); + trigger::register(&mut dispatcher, registry); + scoreboard::register(&mut dispatcher, registry); dispatcher } diff --git a/pumpkin/src/command/commands/scoreboard.rs b/pumpkin/src/command/commands/scoreboard.rs new file mode 100644 index 000000000..e9ef2f261 --- /dev/null +++ b/pumpkin/src/command/commands/scoreboard.rs @@ -0,0 +1,209 @@ +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::string::StringArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::objective::ObjectiveArgumentType; +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::world::scoreboard::{ScoreboardObjective, ScoreboardScore}; +use pumpkin_data::translation; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::RenderType; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; + +const DESCRIPTION: &str = "Manages scoreboard objectives and players."; +const PERMISSION: &str = "minecraft:command.scoreboard"; + +const ARG_OBJECTIVE: &str = "objective"; +const ARG_CRITERION: &str = "criterion"; +const ARG_DISPLAY_NAME: &str = "display_name"; +const ARG_TARGETS: &str = "targets"; + +const DUPLICATE_OBJECTIVE_ERROR: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SCOREBOARD_OBJECTIVES_ADD_DUPLICATE, + translation::java::COMMANDS_SCOREBOARD_OBJECTIVES_ADD_DUPLICATE, +); + +const INVALID_ENABLE_ERROR: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SCOREBOARD_PLAYERS_ENABLE_INVALID, + translation::java::COMMANDS_SCOREBOARD_PLAYERS_ENABLE_INVALID, +); + +const FAILED_ENABLE_ERROR: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SCOREBOARD_PLAYERS_ENABLE_FAILED, + translation::java::COMMANDS_SCOREBOARD_PLAYERS_ENABLE_FAILED, +); + +struct ObjectivesAddExecutor { + has_display_name: bool, +} + +impl CommandExecutor for ObjectivesAddExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let objective_name = StringArgumentType::get(context, ARG_OBJECTIVE)?; + let criterion = StringArgumentType::get(context, ARG_CRITERION)?; + + let display_name = if self.has_display_name { + TextComponent::text(StringArgumentType::get(context, ARG_DISPLAY_NAME)?.to_string()) + } else { + TextComponent::text(objective_name.to_string()) + }; + + let world = context.world(); + let mut scoreboard = world.scoreboard.lock().await; + + if scoreboard.get_objectives().contains_key(objective_name) { + return Err(DUPLICATE_OBJECTIVE_ERROR.create_without_context()); + } + + let obj_name_static = Box::leak(objective_name.to_string().into_boxed_str()); + let criterion_static = Box::leak(criterion.to_string().into_boxed_str()); + + let new_objective = ScoreboardObjective::new( + obj_name_static, + display_name.clone(), + RenderType::Integer, + None, + criterion_static, + ); + + scoreboard.add_objective(world, new_objective).await; + + context + .source + .send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SCOREBOARD_OBJECTIVES_ADD_SUCCESS, + translation::java::COMMANDS_SCOREBOARD_OBJECTIVES_ADD_SUCCESS, + [display_name], + ), + true, + ) + .await; + + Ok(1) + }) + } +} + +struct PlayersEnableExecutor; + +impl CommandExecutor for PlayersEnableExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let targets = EntityArgumentType::get_players(context, ARG_TARGETS).await?; + let objective_name = ObjectiveArgumentType::get(context, ARG_OBJECTIVE)?; + + let world = context.world(); + let mut scoreboard = world.scoreboard.lock().await; + + let objective = scoreboard + .get_objectives() + .get(objective_name) + .ok_or_else(|| INVALID_ENABLE_ERROR.create_without_context())?; + + if objective.criterion != "trigger" { + return Err(INVALID_ENABLE_ERROR.create_without_context()); + } + + let objective_display_name = objective.display_name.clone(); + + let mut enabled_count = 0; + for player in &targets { + let player_name = &player.gameprofile.name; + let current_score = scoreboard + .get_scores() + .get(objective_name) + .and_then(|m| m.get(player_name)); + + let is_already_enabled = current_score.is_some_and(|s| !s.locked); + + if !is_already_enabled { + let value = current_score.map_or(0, |s| s.value.0); + let display_name = current_score.and_then(|s| s.display_name.clone()); + let number_format = current_score.and_then(|s| s.number_format.clone()); + + let updated_score = ScoreboardScore { + entity_name: Box::leak(player_name.clone().into_boxed_str()), + objective_name: Box::leak(objective_name.to_string().into_boxed_str()), + value: VarInt(value), + display_name, + number_format, + locked: false, + }; + + scoreboard.update_score(world, updated_score).await; + enabled_count += 1; + } + } + + if enabled_count == 0 { + return Err(FAILED_ENABLE_ERROR.create_without_context()); + } + + let msg = if targets.len() == 1 { + TextComponent::translate_cross( + translation::java::COMMANDS_SCOREBOARD_PLAYERS_ENABLE_SUCCESS_SINGLE, + translation::java::COMMANDS_SCOREBOARD_PLAYERS_ENABLE_SUCCESS_SINGLE, + [ + objective_display_name, + TextComponent::text(targets[0].gameprofile.name.clone()), + ], + ) + } else { + TextComponent::translate_cross( + translation::java::COMMANDS_SCOREBOARD_PLAYERS_ENABLE_SUCCESS_MULTIPLE, + translation::java::COMMANDS_SCOREBOARD_PLAYERS_ENABLE_SUCCESS_MULTIPLE, + [ + objective_display_name, + TextComponent::text(targets.len().to_string()), + ], + ) + }; + + context.source.send_feedback(msg, true).await; + + Ok(enabled_count) + }) + } +} + +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("scoreboard", DESCRIPTION) + .requires(PERMISSION) + .then( + literal("objectives").then( + literal("add").then( + argument(ARG_OBJECTIVE, StringArgumentType::SingleWord).then( + argument(ARG_CRITERION, StringArgumentType::SingleWord) + .executes(ObjectivesAddExecutor { + has_display_name: false, + }) + .then( + argument(ARG_DISPLAY_NAME, StringArgumentType::GreedyPhrase) + .executes(ObjectivesAddExecutor { + has_display_name: true, + }), + ), + ), + ), + ), + ) + .then(literal("players").then(literal("enable").then( + argument(ARG_TARGETS, EntityArgumentType::Players).then( + argument(ARG_OBJECTIVE, ObjectiveArgumentType).executes(PlayersEnableExecutor), + ), + ))), + ); +} diff --git a/pumpkin/src/command/commands/trigger.rs b/pumpkin/src/command/commands/trigger.rs new file mode 100644 index 000000000..34bf84876 --- /dev/null +++ b/pumpkin/src/command/commands/trigger.rs @@ -0,0 +1,256 @@ +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::objective::ObjectiveArgumentType; +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::world::scoreboard::ScoreboardScore; +use pumpkin_data::translation; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; + +const DESCRIPTION: &str = "Modifies a trigger scoreboard objective."; +const PERMISSION: &str = "minecraft:command.trigger"; + +const ARG_OBJECTIVE: &str = "objective"; +const ARG_VALUE: &str = "value"; + +const INVALID_TRIGGER_ERROR: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_TRIGGER_FAILED_INVALID, + translation::java::COMMANDS_TRIGGER_FAILED_INVALID, +); + +const UNPRIMED_TRIGGER_ERROR: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_TRIGGER_FAILED_UNPRIMED, + translation::java::COMMANDS_TRIGGER_FAILED_UNPRIMED, +); + +struct SimpleTriggerExecutor; + +impl CommandExecutor for SimpleTriggerExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let player = context.source.player_or_err()?; + let player_name = &player.gameprofile.name; + let objective_name = ObjectiveArgumentType::get(context, ARG_OBJECTIVE)?; + + let world = context.world(); + let mut scoreboard = world.scoreboard.lock().await; + + let objective = scoreboard + .get_objectives() + .get(objective_name) + .ok_or_else(|| INVALID_TRIGGER_ERROR.create_without_context())?; + + if objective.criterion != "trigger" { + return Err(INVALID_TRIGGER_ERROR.create_without_context()); + } + + let objective_display_name = objective.display_name.clone(); + + let is_locked = scoreboard + .get_scores() + .get(objective_name) + .and_then(|m| m.get(player_name)) + .is_none_or(|s| s.locked); + + if is_locked { + return Err(UNPRIMED_TRIGGER_ERROR.create_without_context()); + } + + let current_value = scoreboard + .get_scores() + .get(objective_name) + .and_then(|m| m.get(player_name)) + .map_or(0, |s| s.value.0); + + let new_value = current_value + 1; + + let updated_score = ScoreboardScore { + entity_name: Box::leak(player_name.clone().into_boxed_str()), + objective_name: Box::leak(objective_name.to_string().into_boxed_str()), + value: VarInt(new_value), + display_name: None, + number_format: None, + locked: true, + }; + + scoreboard.update_score(world, updated_score).await; + + context + .source + .send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_TRIGGER_SIMPLE_SUCCESS, + translation::java::COMMANDS_TRIGGER_SIMPLE_SUCCESS, + [objective_display_name], + ), + false, + ) + .await; + + Ok(new_value) + }) + } +} + +struct AddTriggerExecutor; + +impl CommandExecutor for AddTriggerExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let player = context.source.player_or_err()?; + let player_name = &player.gameprofile.name; + let objective_name = ObjectiveArgumentType::get(context, ARG_OBJECTIVE)?; + let value = IntegerArgumentType::get(context, ARG_VALUE)?; + + let world = context.world(); + let mut scoreboard = world.scoreboard.lock().await; + + let objective = scoreboard + .get_objectives() + .get(objective_name) + .ok_or_else(|| INVALID_TRIGGER_ERROR.create_without_context())?; + + if objective.criterion != "trigger" { + return Err(INVALID_TRIGGER_ERROR.create_without_context()); + } + + let objective_display_name = objective.display_name.clone(); + + let is_locked = scoreboard + .get_scores() + .get(objective_name) + .and_then(|m| m.get(player_name)) + .is_none_or(|s| s.locked); + + if is_locked { + return Err(UNPRIMED_TRIGGER_ERROR.create_without_context()); + } + + let current_value = scoreboard + .get_scores() + .get(objective_name) + .and_then(|m| m.get(player_name)) + .map_or(0, |s| s.value.0); + + let new_value = current_value + value; + + let updated_score = ScoreboardScore { + entity_name: Box::leak(player_name.clone().into_boxed_str()), + objective_name: Box::leak(objective_name.to_string().into_boxed_str()), + value: VarInt(new_value), + display_name: None, + number_format: None, + locked: true, + }; + + scoreboard.update_score(world, updated_score).await; + + context + .source + .send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_TRIGGER_ADD_SUCCESS, + translation::java::COMMANDS_TRIGGER_ADD_SUCCESS, + [ + objective_display_name, + TextComponent::text(value.to_string()), + ], + ), + false, + ) + .await; + + Ok(new_value) + }) + } +} + +struct SetTriggerExecutor; + +impl CommandExecutor for SetTriggerExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let player = context.source.player_or_err()?; + let player_name = &player.gameprofile.name; + let objective_name = ObjectiveArgumentType::get(context, ARG_OBJECTIVE)?; + let value = IntegerArgumentType::get(context, ARG_VALUE)?; + + let world = context.world(); + let mut scoreboard = world.scoreboard.lock().await; + + let objective = scoreboard + .get_objectives() + .get(objective_name) + .ok_or_else(|| INVALID_TRIGGER_ERROR.create_without_context())?; + + if objective.criterion != "trigger" { + return Err(INVALID_TRIGGER_ERROR.create_without_context()); + } + + let objective_display_name = objective.display_name.clone(); + + let is_locked = scoreboard + .get_scores() + .get(objective_name) + .and_then(|m| m.get(player_name)) + .is_none_or(|s| s.locked); + + if is_locked { + return Err(UNPRIMED_TRIGGER_ERROR.create_without_context()); + } + + let updated_score = ScoreboardScore { + entity_name: Box::leak(player_name.clone().into_boxed_str()), + objective_name: Box::leak(objective_name.to_string().into_boxed_str()), + value: VarInt(value), + display_name: None, + number_format: None, + locked: true, + }; + + scoreboard.update_score(world, updated_score).await; + + context + .source + .send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_TRIGGER_SET_SUCCESS, + translation::java::COMMANDS_TRIGGER_SET_SUCCESS, + [ + objective_display_name, + TextComponent::text(value.to_string()), + ], + ), + false, + ) + .await; + + Ok(value) + }) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Allow, + )); + + dispatcher.register( + command("trigger", DESCRIPTION).requires(PERMISSION).then( + argument(ARG_OBJECTIVE, ObjectiveArgumentType) + .executes(SimpleTriggerExecutor) + .then(literal("add").then( + argument(ARG_VALUE, IntegerArgumentType::any()).executes(AddTriggerExecutor), + )) + .then(literal("set").then( + argument(ARG_VALUE, IntegerArgumentType::any()).executes(SetTriggerExecutor), + )), + ), + ); +} diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs index 5a0d42bbe..1350be907 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs @@ -41,8 +41,13 @@ impl scoreboard::HostScoreboard for PluginHostState { RenderType::Hearts => pumpkin_protocol::java::client::play::RenderType::Hearts, }; - let objective = - ScoreboardObjective::new(Box::leak(name.into_boxed_str()), display_name, rt, None); + let objective = ScoreboardObjective::new( + Box::leak(name.into_boxed_str()), + display_name, + rt, + None, + "dummy", + ); world .scoreboard .lock() diff --git a/pumpkin/src/world/scoreboard.rs b/pumpkin/src/world/scoreboard.rs index 5ecf2cee5..fe01bba29 100644 --- a/pumpkin/src/world/scoreboard.rs +++ b/pumpkin/src/world/scoreboard.rs @@ -26,6 +26,16 @@ pub struct Scoreboard { } impl Scoreboard { + #[must_use] + pub const fn get_objectives(&self) -> &HashMap> { + &self.objectives + } + + #[must_use] + pub const fn get_scores(&self) -> &HashMap>> { + &self.scores + } + async fn broadcast_editioned( world: &World, je_packet: &J, @@ -281,6 +291,7 @@ pub struct ScoreboardObjective<'a> { pub display_name: TextComponent, pub render_type: RenderType, pub number_format: Option, + pub criterion: &'a str, } impl<'a> ScoreboardObjective<'a> { @@ -290,12 +301,14 @@ impl<'a> ScoreboardObjective<'a> { display_name: TextComponent, render_type: RenderType, number_format: Option, + criterion: &'a str, ) -> Self { Self { name, display_name, render_type, number_format, + criterion, } } } @@ -306,6 +319,7 @@ pub struct ScoreboardScore<'a> { pub value: VarInt, pub display_name: Option, pub number_format: Option, + pub locked: bool, } impl<'a> ScoreboardScore<'a> { @@ -323,6 +337,7 @@ impl<'a> ScoreboardScore<'a> { value, display_name, number_format, + locked: true, } } }