Skip to content

Commit 4929c59

Browse files
authored
feat(pumpkin): add /random command (Pumpkin-MC#2396)
Implements the vanilla /random command (tracked in Pumpkin-MC#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.
1 parent 650c614 commit 4929c59

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

pumpkin/src/command/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ mod playsound;
3838
mod plugin;
3939
mod plugins;
4040
mod pumpkin;
41+
mod random;
4142
mod rotate;
4243
mod say;
4344
mod seed;
@@ -152,6 +153,7 @@ pub async fn default_dispatcher(
152153
help::register(&mut dispatcher, registry);
153154
kill::register(&mut dispatcher, registry);
154155
op::register(&mut dispatcher, registry);
156+
random::register(&mut dispatcher, registry);
155157
list::register(&mut dispatcher, registry);
156158
seed::register(&mut dispatcher, registry);
157159
setidletimeout::register(&mut dispatcher, registry);
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use pumpkin_data::translation;
2+
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
3+
use pumpkin_util::text::TextComponent;
4+
5+
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
6+
use crate::command::argument_types::range::IntRangeArgumentType;
7+
use crate::command::context::command_context::CommandContext;
8+
use crate::command::errors::error_types::CommandErrorType;
9+
use crate::command::node::dispatcher::CommandDispatcher;
10+
use crate::command::node::{CommandExecutor, CommandExecutorResult};
11+
12+
const DESCRIPTION: &str = "Draws a random value.";
13+
14+
const PERMISSION: &str = "minecraft:command.random";
15+
16+
const ARG_RANGE: &str = "range";
17+
18+
/// The largest allowed range span, matching Vanilla (`i32::MAX - 1`).
19+
const MAX_RANGE_SPAN: i64 = (i32::MAX - 1) as i64;
20+
21+
const RANGE_TOO_LARGE_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new(
22+
translation::java::COMMANDS_RANDOM_ERROR_RANGE_TOO_LARGE,
23+
translation::java::COMMANDS_RANDOM_ERROR_RANGE_TOO_LARGE,
24+
);
25+
26+
const RANGE_TOO_SMALL_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new(
27+
translation::java::COMMANDS_RANDOM_ERROR_RANGE_TOO_SMALL,
28+
translation::java::COMMANDS_RANDOM_ERROR_RANGE_TOO_SMALL,
29+
);
30+
31+
struct RandomExecutor {
32+
/// Whether the result is announced to every player (`roll`) instead of
33+
/// only the command source (`value`).
34+
roll: bool,
35+
}
36+
37+
impl CommandExecutor for RandomExecutor {
38+
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
39+
Box::pin(async move {
40+
let bounds = IntRangeArgumentType::get(context, ARG_RANGE)?;
41+
let min = bounds.min().unwrap_or(i32::MIN);
42+
let max = bounds.max().unwrap_or(i32::MAX);
43+
44+
let span = i64::from(max) - i64::from(min);
45+
if span == 0 {
46+
return Err(RANGE_TOO_SMALL_ERROR_TYPE.create_without_context());
47+
}
48+
if span >= MAX_RANGE_SPAN {
49+
return Err(RANGE_TOO_LARGE_ERROR_TYPE.create_without_context());
50+
}
51+
52+
let result = rand::random_range(min..=max);
53+
54+
if self.roll {
55+
let msg = TextComponent::translate_cross(
56+
translation::java::COMMANDS_RANDOM_ROLL,
57+
translation::java::COMMANDS_RANDOM_ROLL,
58+
[
59+
context.source.display_name.clone(),
60+
TextComponent::text(result.to_string()),
61+
TextComponent::text(min.to_string()),
62+
TextComponent::text(max.to_string()),
63+
],
64+
);
65+
for player in context.server().get_all_players() {
66+
player.send_system_message(&msg).await;
67+
}
68+
// Also show the roll on the console (or another non-player source).
69+
if context.source.player_or_none().is_none() {
70+
context.source.send_message(msg).await;
71+
}
72+
} else {
73+
context
74+
.source
75+
.send_feedback(
76+
TextComponent::translate_cross(
77+
translation::java::COMMANDS_RANDOM_SAMPLE_SUCCESS,
78+
translation::java::COMMANDS_RANDOM_SAMPLE_SUCCESS,
79+
[TextComponent::text(result.to_string())],
80+
),
81+
false,
82+
)
83+
.await;
84+
}
85+
86+
Ok(result)
87+
})
88+
}
89+
}
90+
91+
pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) {
92+
registry.register_permission_or_panic(Permission::new(
93+
PERMISSION,
94+
DESCRIPTION,
95+
// Vanilla allows every player to use `/random value` and `/random roll`.
96+
PermissionDefault::Allow,
97+
));
98+
99+
dispatcher.register(
100+
command("random", DESCRIPTION)
101+
.requires(PERMISSION)
102+
.then(literal("value").then(
103+
argument(ARG_RANGE, IntRangeArgumentType).executes(RandomExecutor { roll: false }),
104+
))
105+
.then(literal("roll").then(
106+
argument(ARG_RANGE, IntRangeArgumentType).executes(RandomExecutor { roll: true }),
107+
)),
108+
);
109+
}

0 commit comments

Comments
 (0)