Pumpkin/pumpkin/src/command/commands/bossbar.rs
2026-05-03 21:31:46 +02:00

672 lines
25 KiB
Rust

use crate::command::args::bool::BoolArgConsumer;
use crate::command::args::bossbar_color::BossbarColorArgumentConsumer;
use crate::command::args::bossbar_style::BossbarStyleArgumentConsumer;
use crate::command::args::bounded_num::BoundedNumArgumentConsumer;
use crate::command::args::players::PlayersArgumentConsumer;
use crate::command::args::resource_location::ResourceLocationArgumentConsumer;
use crate::command::args::{ConsumedArgs, FindArg, FindArgDefaultName};
use crate::command::args::textcomponent::TextComponentArgConsumer;
use crate::command::dispatcher::CommandError;
use crate::command::tree::CommandTree;
use crate::command::tree::builder::{argument, argument_default_name, literal};
use crate::command::{CommandExecutor, CommandResult, CommandSender};
use crate::world::bossbar::Bossbar;
use crate::world::custom_bossbar::BossbarUpdateError;
use pumpkin_data::translation;
use pumpkin_util::text::TextComponent;
use pumpkin_util::text::hover::HoverEvent;
use uuid::Uuid;
const NAMES: [&str; 1] = ["bossbar"];
const DESCRIPTION: &str = "Display bossbar";
const ARG_NAME: &str = "name";
const ARG_VISIBLE: &str = "visible";
const fn autocomplete_consumer() -> ResourceLocationArgumentConsumer {
// TODO: Add autocompletion when implemented properly
ResourceLocationArgumentConsumer
}
enum CommandValueGet {
Max,
Players,
Value,
Visible,
}
enum CommandValueSet {
Color,
Max,
Name,
Players(bool),
Style,
Value,
Visible,
}
struct AddExecutor;
impl CommandExecutor for AddExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
server: &'a crate::server::Server,
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let mut namespace = autocomplete_consumer()
.find_arg_default_name(args)?
.to_string();
if !namespace.contains(':') {
namespace = format!("minecraft:{namespace}");
}
let text_component = TextComponentArgConsumer::find_arg(args, ARG_NAME)?;
if server.bossbars.lock().await.has_bossbar(&namespace) {
return Result::Err(CommandError::CommandFailed(TextComponent::translate_cross(
"commands.bossbar.create.failed",
"commands.bossbar.create.failed",
[TextComponent::text(namespace.clone())],
)));
}
let bossbar = Bossbar::new(text_component);
let mut bossbars = server.bossbars.lock().await;
bossbars.create_bossbar(namespace.clone(), bossbar.clone());
let new_size = bossbars.get_bossbars_len();
drop(bossbars);
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.create.success",
"commands.bossbar.create.success",
[bossbar_prefix(bossbar.title.clone(), namespace.clone())],
))
.await;
Ok(new_size as i32)
})
}
}
struct GetExecutor(CommandValueGet);
impl CommandExecutor for GetExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
server: &'a crate::server::Server,
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let namespace = autocomplete_consumer()
.find_arg_default_name(args)?
.to_string();
let Some(bossbar) = server.bossbars.lock().await.get_bossbar(&namespace) else {
return Err(handle_bossbar_error(
BossbarUpdateError::InvalidResourceLocation(namespace.clone()),
));
};
match self.0 {
CommandValueGet::Max => {
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.get.max",
"commands.bossbar.get.max",
[
bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.clone(),
),
TextComponent::text(bossbar.max.to_string()),
],
))
.await;
Ok(bossbar.max)
}
CommandValueGet::Players => Ok(bossbar.players.len() as i32),
CommandValueGet::Value => {
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.get.value",
"commands.bossbar.get.value",
[
bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.clone(),
),
TextComponent::text(bossbar.value.to_string()),
],
))
.await;
Ok(bossbar.value)
}
CommandValueGet::Visible => {
let state = if bossbar.visible {
"commands.bossbar.get.visible.visible"
} else {
"commands.bossbar.get.visible.hidden"
};
sender
.send_message(TextComponent::translate_cross(
state,
state,
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.clone(),
)],
))
.await;
Ok(bossbar.visible as i32)
}
}
})
}
}
struct ListExecutor;
impl CommandExecutor for ListExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
server: &'a crate::server::Server,
_args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let bossbars = server.bossbars.lock().await.get_all_bossbars();
if bossbars.is_empty() {
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.list.bars.none",
"commands.bossbar.list.bars.none",
[],
))
.await;
return Ok(0);
}
let mut bossbars_text = TextComponent::empty();
for (i, bossbar) in bossbars.iter().enumerate() {
if i == 0 {
bossbars_text = bossbars_text.add_child(bossbar_prefix(
bossbar.bossbar_data.title.clone(),
bossbar.namespace.clone(),
));
} else {
bossbars_text = bossbars_text.add_child(TextComponent::text(", ").add_child(
bossbar_prefix(
bossbar.bossbar_data.title.clone(),
bossbar.namespace.clone(),
),
));
}
}
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.list.bars.some",
"commands.bossbar.list.bars.some",
[
TextComponent::text(bossbars.len().to_string()),
bossbars_text,
],
))
.await;
Ok(bossbars.len() as i32)
})
}
}
struct RemoveExecutor;
impl CommandExecutor for RemoveExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
server: &'a crate::server::Server,
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let namespace = autocomplete_consumer()
.find_arg_default_name(args)?
.to_string();
let Some(bossbar) = server.bossbars.lock().await.get_bossbar(&namespace) else {
return Err(handle_bossbar_error(
BossbarUpdateError::InvalidResourceLocation(namespace),
));
};
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.remove.success",
"commands.bossbar.remove.success",
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.clone(),
)],
))
.await;
let error = {
match server
.bossbars
.lock()
.await
.remove_bossbar(server, namespace.clone())
.await
{
Ok(()) => return Ok(server.bossbars.lock().await.get_bossbars_len() as i32),
Err(error) => error,
}
};
Err(handle_bossbar_error(error))
})
}
}
struct SetExecutor(CommandValueSet);
impl CommandExecutor for SetExecutor {
#[expect(clippy::too_many_lines)]
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
server: &'a crate::server::Server,
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let namespace = autocomplete_consumer().find_arg_default_name(args)?;
let Some(bossbar) = server.bossbars.lock().await.get_bossbar(namespace) else {
return Err(handle_bossbar_error(
BossbarUpdateError::InvalidResourceLocation(namespace.to_string()),
));
};
match self.0 {
CommandValueSet::Color => {
let color = BossbarColorArgumentConsumer.find_arg_default_name(args)?;
match server
.bossbars
.lock()
.await
.update_color(server, namespace.to_string(), *color)
.await
{
Ok(()) => {}
Err(err) => {
return Err(handle_bossbar_error(err));
}
}
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.color.success",
"commands.bossbar.set.color.success",
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
)],
))
.await;
Ok(0)
}
CommandValueSet::Max => {
let Ok(max_value) = max_value_consumer().find_arg_default_name(args)? else {
return Err(CommandError::CommandFailed(TextComponent::translate_cross(
"parsing.int.invalid",
"parsing.int.invalid",
[TextComponent::text(i32::MAX.to_string())],
)));
};
match server
.bossbars
.lock()
.await
.update_health(server, namespace.to_string(), max_value, bossbar.value)
.await
{
Ok(()) => {}
Err(err) => {
return Err(handle_bossbar_error(err));
}
}
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.max.success",
"commands.bossbar.set.max.success",
[
bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
),
TextComponent::text(max_value.to_string()),
],
))
.await;
Ok(max_value)
}
CommandValueSet::Name => {
let text_component = TextComponentArgConsumer::find_arg(args, ARG_NAME)?;
match server
.bossbars
.lock()
.await
.update_name(server, namespace, text_component.clone())
.await
{
Ok(()) => {}
Err(err) => {
return Err(handle_bossbar_error(err));
}
}
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.name.success",
"commands.bossbar.set.name.success",
[bossbar_prefix(text_component, namespace.to_string())],
))
.await;
Ok(0)
}
CommandValueSet::Players(has_players) => {
if !has_players {
match server
.bossbars
.lock()
.await
.update_players(server, namespace.to_string(), vec![])
.await
{
Ok(()) => {}
Err(err) => {
return Err(handle_bossbar_error(err));
}
}
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.players.success.none",
"commands.bossbar.set.players.success.none",
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
)],
))
.await;
return Ok(0);
}
let targets = PlayersArgumentConsumer.find_arg_default_name(args)?;
let players: Vec<Uuid> =
targets.iter().map(|player| player.gameprofile.id).collect();
let count = players.len();
match server
.bossbars
.lock()
.await
.update_players(server, namespace.to_string(), players)
.await
{
Ok(()) => {}
Err(err) => {
return Err(handle_bossbar_error(err));
}
}
let player_names = targets
.iter()
.map(|p| p.gameprofile.name.as_str())
.collect::<Vec<_>>()
.join(", ");
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.players.success.some",
"commands.bossbar.set.players.success.some",
[
bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
),
TextComponent::text(count.to_string()),
TextComponent::text(player_names),
],
))
.await;
Ok(count as i32)
}
CommandValueSet::Style => {
let style = BossbarStyleArgumentConsumer.find_arg_default_name(args)?;
match server
.bossbars
.lock()
.await
.update_division(server, namespace.to_string(), *style)
.await
{
Ok(()) => {}
Err(err) => {
return Err(handle_bossbar_error(err));
}
}
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.style.success",
"commands.bossbar.set.style.success",
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
)],
))
.await;
Ok(0)
}
CommandValueSet::Value => {
let Ok(value) = value_consumer().find_arg_default_name(args)? else {
return Err(CommandError::CommandFailed(TextComponent::translate_cross(
"parsing.int.invalid",
"parsing.int.invalid",
[TextComponent::text(i32::MAX.to_string())],
)));
};
match server
.bossbars
.lock()
.await
.update_health(server, namespace.to_string(), bossbar.max, value)
.await
{
Ok(()) => {}
Err(err) => {
return Err(handle_bossbar_error(err));
}
}
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.value.success",
"commands.bossbar.set.value.success",
[
bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
),
TextComponent::text(value.to_string()),
],
))
.await;
Ok(value)
}
CommandValueSet::Visible => {
let visibility = BoolArgConsumer::find_arg(args, ARG_VISIBLE)?;
match server
.bossbars
.lock()
.await
.update_visibility(server, namespace.to_string(), visibility)
.await
{
Ok(()) => {}
Err(err) => {
return Err(handle_bossbar_error(err));
}
}
let state = if visibility {
"commands.bossbar.set.visible.success.visible"
} else {
"commands.bossbar.set.visible.success.hidden"
};
sender
.send_message(TextComponent::translate_cross(
state,
state,
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
)],
))
.await;
Ok(visibility as i32)
}
}
})
}
}
const fn max_value_consumer() -> BoundedNumArgumentConsumer<i32> {
BoundedNumArgumentConsumer::new().min(1).name("max")
}
const fn value_consumer() -> BoundedNumArgumentConsumer<i32> {
BoundedNumArgumentConsumer::new().min(0).name("value")
}
pub fn init_command_tree() -> CommandTree {
CommandTree::new(NAMES, DESCRIPTION)
.then(
literal("add").then(
argument_default_name(autocomplete_consumer())
.then(argument(ARG_NAME, TextComponentArgConsumer).execute(AddExecutor)),
),
)
.then(
literal("get").then(
argument_default_name(autocomplete_consumer())
.then(literal("max").execute(GetExecutor(CommandValueGet::Max)))
.then(literal("players").execute(GetExecutor(CommandValueGet::Players)))
.then(literal("value").execute(GetExecutor(CommandValueGet::Value)))
.then(literal("visible").execute(GetExecutor(CommandValueGet::Visible))),
),
)
.then(literal("list").execute(ListExecutor))
.then(
literal("remove")
.then(argument_default_name(autocomplete_consumer()).execute(RemoveExecutor)),
)
.then(
literal("set").then(
argument_default_name(autocomplete_consumer())
.then(
literal("color").then(
argument_default_name(BossbarColorArgumentConsumer)
.execute(SetExecutor(CommandValueSet::Color)),
),
)
.then(
literal("max").then(
argument_default_name(max_value_consumer())
.execute(SetExecutor(CommandValueSet::Max)),
),
)
.then(
literal("name").then(
argument(ARG_NAME, TextComponentArgConsumer)
.execute(SetExecutor(CommandValueSet::Name)),
),
)
.then(
literal("players")
.then(
argument_default_name(PlayersArgumentConsumer)
.execute(SetExecutor(CommandValueSet::Players(true))),
)
.execute(SetExecutor(CommandValueSet::Players(false))),
)
.then(
literal("style").then(
argument_default_name(BossbarStyleArgumentConsumer)
.execute(SetExecutor(CommandValueSet::Style)),
),
)
.then(
literal("value").then(
argument_default_name(value_consumer())
.execute(SetExecutor(CommandValueSet::Value)),
),
)
.then(
literal("visible").then(
argument(ARG_VISIBLE, BoolArgConsumer)
.execute(SetExecutor(CommandValueSet::Visible)),
),
),
),
)
}
fn bossbar_prefix(title: TextComponent, namespace: String) -> TextComponent {
TextComponent::text("[")
.add_child(title)
.add_child(TextComponent::text("]"))
.hover_event(HoverEvent::show_text(TextComponent::text(namespace)))
}
fn handle_bossbar_error(error: BossbarUpdateError) -> CommandError {
match error {
BossbarUpdateError::InvalidResourceLocation(location) => {
CommandError::CommandFailed(TextComponent::translate_cross(
translation::java::COMMANDS_BOSSBAR_UNKNOWN,
translation::java::COMMANDS_BOSSBAR_UNKNOWN,
[TextComponent::text(location)],
))
}
BossbarUpdateError::NoChanges(value, variation) => {
let key = variation.map_or_else(
|| format!("commands.bossbar.set.{value}.unchanged"),
|var| format!("commands.bossbar.set.{value}.unchanged.{var}"),
);
CommandError::CommandFailed(TextComponent::translate_cross(key.clone(), key, []))
}
}
}