From d78ecce3685f19f7b97ec1791a315f452a4fdad3 Mon Sep 17 00:00:00 2001 From: Laptop59 <90901102+Laptop59@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:27:55 +0530 Subject: [PATCH] feat(command): reimplement `/list` (#1968) * reimplemented `/list` & new join methods for `TextComponent` * switched to use `register_permission_or_panic` --- pumpkin-util/src/text/mod.rs | 47 +++++++++++ pumpkin/src/command/commands/list.rs | 112 ++++++++++++++++++--------- pumpkin/src/command/commands/mod.rs | 9 +-- 3 files changed, 124 insertions(+), 44 deletions(-) diff --git a/pumpkin-util/src/text/mod.rs b/pumpkin-util/src/text/mod.rs index dd80389fa..994a23dcd 100644 --- a/pumpkin-util/src/text/mod.rs +++ b/pumpkin-util/src/text/mod.rs @@ -12,6 +12,7 @@ use serde::de::{Error, MapAccess, SeqAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; use std::borrow::Cow; use std::fmt::Formatter; +use std::sync::LazyLock; use style::Style; pub mod click; @@ -875,6 +876,52 @@ impl TextComponent { } } +impl TextComponent { + /// Joins multiple text components into one with a separator containing a gray comma + /// and a space after it. + /// + /// # Arguments + /// - `elements` - The elements to join. + /// + /// # Returns + /// The resultant text component with all the elements joined in it. + #[must_use] + pub fn join_with_comma(elements: Vec) -> Self { + static DEFAULT_SEPARATOR: LazyLock = LazyLock::new(|| { + TextComponent::text(", ").color(Color::Named(color::NamedColor::Gray)) + }); + + Self::join(elements, &DEFAULT_SEPARATOR) + } + + /// Joins multiple text components into one with the given separator text component. + /// Use [`TextComponent::join_with_comma`] instead if you just want to join text components with + /// a comma in between. + /// + /// # Arguments + /// - `elements` - The elements to join. + /// - `separator` - The separator to use for joining the elements provided. + /// + /// # Returns + /// The resultant text component with all the elements joined in it. + #[must_use] + pub fn join(elements: Vec, separator: &Self) -> Self { + let mut result = Self::empty(); + let mut first = true; + + for element in elements { + if !first { + result = result.add_child(separator.clone()); + } + + result = result.add_child(element); + first = false; + } + + result + } +} + /// The content type of the text component. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(untagged)] diff --git a/pumpkin/src/command/commands/list.rs b/pumpkin/src/command/commands/list.rs index d521a0f20..f095663f2 100644 --- a/pumpkin/src/command/commands/list.rs +++ b/pumpkin/src/command/commands/list.rs @@ -1,58 +1,98 @@ use std::sync::Arc; -use pumpkin_util::text::TextComponent; +use pumpkin_data::translation::{COMMANDS_LIST_NAMEANDID, COMMANDS_LIST_PLAYERS}; +use pumpkin_util::{ + permission::{Permission, PermissionDefault, PermissionRegistry}, + text::TextComponent, +}; use crate::{ command::{ - CommandExecutor, CommandResult, CommandSender, args::ConsumedArgs, tree::CommandTree, + argument_builder::{ArgumentBuilder, command, literal}, + context::command_context::CommandContext, + node::{CommandExecutor, CommandExecutorResult, dispatcher::CommandDispatcher}, }, - entity::player::Player, + entity::{EntityBase, EntityBaseFuture, player::Player}, }; -const NAMES: [&str; 1] = ["list"]; - const DESCRIPTION: &str = "Print the list of online players."; -struct Executor; +const PERMISSION: &str = "minecraft:command.list"; -impl CommandExecutor for Executor { - fn execute<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a crate::server::Server, - _args: &'a ConsumedArgs<'a>, - ) -> CommandResult<'a> { +enum ListMode { + Names, + Uuids, +} + +struct ListCommandExecutor(ListMode); + +impl CommandExecutor for ListCommandExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { Box::pin(async move { - let players: Vec> = server.get_all_players(); - let players_len = players.len() as i32; + let players: Vec> = context.server().get_all_players(); + let players_len = players.len(); - sender - .send_message(TextComponent::translate( - "commands.list.players", - [ - TextComponent::text(players.len().to_string()), - TextComponent::text(server.basic_config.max_players.to_string()), - TextComponent::text(get_player_names(&players)), - ], - )) + let list = match self.0 { + ListMode::Names => get_player_names(&players).await, + ListMode::Uuids => get_player_names_and_ids(&players), + }; + + context + .source + .send_feedback( + TextComponent::translate( + COMMANDS_LIST_PLAYERS, + [ + TextComponent::text(players_len.to_string()), + TextComponent::text( + context.server().basic_config.max_players.to_string(), + ), + list, + ], + ), + false, + ) .await; - Ok(players_len) + Ok(players_len as i32) }) } } -fn get_player_names(players: &[Arc]) -> String { - let mut names = String::new(); - for player in players { - if !names.is_empty() { - names.push_str(", "); - } - names.push_str(&player.gameprofile.name); - } - names +async fn get_player_names(players: &[Arc]) -> TextComponent { + let display_name_futures: Vec> = + players.iter().map(|p| p.get_display_name()).collect(); + let display_names = futures::future::join_all(display_name_futures).await; + TextComponent::join_with_comma(display_names) } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).execute(Executor) +fn get_player_names_and_ids(players: &[Arc]) -> TextComponent { + let names_and_ids = players + .iter() + .map(|p| { + TextComponent::translate( + COMMANDS_LIST_NAMEANDID, + &[ + p.get_name(), + TextComponent::text(p.gameprofile.id.to_string()), + ], + ) + }) + .collect(); + TextComponent::join_with_comma(names_and_ids) +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Allow, + )); + + dispatcher.register( + command("list", DESCRIPTION) + .requires(PERMISSION) + .then(literal("uuids").executes(ListCommandExecutor(ListMode::Uuids))) + .executes(ListCommandExecutor(ListMode::Names)), + ); } diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 4b5a148b0..77698f027 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -72,7 +72,6 @@ pub async fn default_dispatcher( // Zero dispatcher.register(pumpkin::init_command_tree(), "pumpkin:command.pumpkin"); - dispatcher.register(list::init_command_tree(), "minecraft:command.list"); dispatcher.register(me::init_command_tree(), "minecraft:command.me"); dispatcher.register(msg::init_command_tree(), "minecraft:command.msg"); // Two @@ -155,6 +154,7 @@ pub async fn default_dispatcher( difficulty::register(&mut dispatcher, registry); help::register(&mut dispatcher, registry); + list::register(&mut dispatcher, registry); seed::register(&mut dispatcher, registry); setidletimeout::register(&mut dispatcher, registry); stop::register(&mut dispatcher, registry); @@ -191,13 +191,6 @@ fn register_level_0_permissions(registry: &mut PermissionRegistry) { PermissionDefault::Allow, )) .unwrap(); - registry - .register_permission(Permission::new( - "minecraft:command.list", - "Lists players that are currently online", - PermissionDefault::Allow, - )) - .unwrap(); registry .register_permission(Permission::new( "minecraft:command.me",