feat(pumpkin): add /random command (#2396)

Implements the vanilla /random command (tracked in #15):

- /random value <range> draws a value and shows it to the source only.
- /random roll <range> draws a value and announces it to every player.
- Range validation matches Vanilla: spans of 0 fail with
  commands.random.error.range_too_small, spans of i32::MAX - 1 or wider
  (including open-ended ranges) fail with range_too_large.

Random sequences (/random reset and the [sequence] argument) are not
part of this change since named sequence storage does not exist yet.
This commit is contained in:
Mcxiaocaibug
2026-07-13 23:38:43 +08:00
committed by GitHub
parent 650c614db9
commit 4929c590ab
2 changed files with 111 additions and 0 deletions

View File

@@ -38,6 +38,7 @@ mod playsound;
mod plugin;
mod plugins;
mod pumpkin;
mod random;
mod rotate;
mod say;
mod seed;
@@ -152,6 +153,7 @@ pub async fn default_dispatcher(
help::register(&mut dispatcher, registry);
kill::register(&mut dispatcher, registry);
op::register(&mut dispatcher, registry);
random::register(&mut dispatcher, registry);
list::register(&mut dispatcher, registry);
seed::register(&mut dispatcher, registry);
setidletimeout::register(&mut dispatcher, registry);

View File

@@ -0,0 +1,109 @@
use pumpkin_data::translation;
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
use pumpkin_util::text::TextComponent;
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
use crate::command::argument_types::range::IntRangeArgumentType;
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};
const DESCRIPTION: &str = "Draws a random value.";
const PERMISSION: &str = "minecraft:command.random";
const ARG_RANGE: &str = "range";
/// The largest allowed range span, matching Vanilla (`i32::MAX - 1`).
const MAX_RANGE_SPAN: i64 = (i32::MAX - 1) as i64;
const RANGE_TOO_LARGE_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new(
translation::java::COMMANDS_RANDOM_ERROR_RANGE_TOO_LARGE,
translation::java::COMMANDS_RANDOM_ERROR_RANGE_TOO_LARGE,
);
const RANGE_TOO_SMALL_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new(
translation::java::COMMANDS_RANDOM_ERROR_RANGE_TOO_SMALL,
translation::java::COMMANDS_RANDOM_ERROR_RANGE_TOO_SMALL,
);
struct RandomExecutor {
/// Whether the result is announced to every player (`roll`) instead of
/// only the command source (`value`).
roll: bool,
}
impl CommandExecutor for RandomExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let bounds = IntRangeArgumentType::get(context, ARG_RANGE)?;
let min = bounds.min().unwrap_or(i32::MIN);
let max = bounds.max().unwrap_or(i32::MAX);
let span = i64::from(max) - i64::from(min);
if span == 0 {
return Err(RANGE_TOO_SMALL_ERROR_TYPE.create_without_context());
}
if span >= MAX_RANGE_SPAN {
return Err(RANGE_TOO_LARGE_ERROR_TYPE.create_without_context());
}
let result = rand::random_range(min..=max);
if self.roll {
let msg = TextComponent::translate_cross(
translation::java::COMMANDS_RANDOM_ROLL,
translation::java::COMMANDS_RANDOM_ROLL,
[
context.source.display_name.clone(),
TextComponent::text(result.to_string()),
TextComponent::text(min.to_string()),
TextComponent::text(max.to_string()),
],
);
for player in context.server().get_all_players() {
player.send_system_message(&msg).await;
}
// Also show the roll on the console (or another non-player source).
if context.source.player_or_none().is_none() {
context.source.send_message(msg).await;
}
} else {
context
.source
.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_RANDOM_SAMPLE_SUCCESS,
translation::java::COMMANDS_RANDOM_SAMPLE_SUCCESS,
[TextComponent::text(result.to_string())],
),
false,
)
.await;
}
Ok(result)
})
}
}
pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) {
registry.register_permission_or_panic(Permission::new(
PERMISSION,
DESCRIPTION,
// Vanilla allows every player to use `/random value` and `/random roll`.
PermissionDefault::Allow,
));
dispatcher.register(
command("random", DESCRIPTION)
.requires(PERMISSION)
.then(literal("value").then(
argument(ARG_RANGE, IntRangeArgumentType).executes(RandomExecutor { roll: false }),
))
.then(literal("roll").then(
argument(ARG_RANGE, IntRangeArgumentType).executes(RandomExecutor { roll: true }),
)),
);
}