Skip to main content

steel_core/command/builtins/
difficulty.rs

1use steel_utils::{Identifier, translations, types::Difficulty};
2use text_components::{TextComponent, translation::Translation};
3
4use super::super::{
5    brigadier::{CommandNodeBuilder, CommandSyntaxError},
6    execution::{CommandSource, SteelCommandContext, SteelCommandRuntime, literal},
7    registration::CommandRegistration,
8};
9use crate::permission::{PermissionContext, PermissionExpr, PermissionKey, PermissionKeyError};
10use crate::player::Player;
11use crate::world::World;
12
13pub(super) fn registration() -> CommandRegistration<CommandSource> {
14    CommandRegistration::new(Identifier::vanilla_static("difficulty"), |_| command())
15}
16
17pub(crate) fn player_can_change_difficulty(player: &Player, world: &World) -> bool {
18    let permission = match difficulty_permission() {
19        Ok(permission) => permission,
20        Err(error) => {
21            log::error!("invalid built-in difficulty permission key: {error}");
22            return false;
23        }
24    };
25    let context = PermissionContext::for_world(world.key.clone());
26    player.has_permission_in(&permission, &context)
27}
28
29fn difficulty_permission() -> Result<PermissionExpr, PermissionKeyError> {
30    PermissionKey::parse("minecraft.command.difficulty").map(PermissionExpr::key)
31}
32
33fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
34    literal("difficulty")
35        .executes(query_difficulty)
36        .then(difficulty_literal("peaceful", Difficulty::Peaceful))
37        .then(difficulty_literal("easy", Difficulty::Easy))
38        .then(difficulty_literal("normal", Difficulty::Normal))
39        .then(difficulty_literal("hard", Difficulty::Hard))
40}
41
42fn difficulty_literal(
43    name: &'static str,
44    difficulty: Difficulty,
45) -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
46    literal(name).executes(move |context| set_difficulty(context, difficulty))
47}
48
49#[expect(
50    clippy::unnecessary_wraps,
51    reason = "Command executors use a shared fallible callback signature."
52)]
53fn query_difficulty(
54    context: &SteelCommandContext<CommandSource>,
55) -> Result<i32, CommandSyntaxError> {
56    let difficulty = context.source().world().difficulty();
57    let message = translations::COMMANDS_DIFFICULTY_QUERY
58        .message([TextComponent::from(difficulty_display_name(difficulty))])
59        .component();
60    context.source().send_success(&message, false);
61    Ok(i32::from(u8::from(difficulty)))
62}
63
64fn set_difficulty(
65    context: &SteelCommandContext<CommandSource>,
66    difficulty: Difficulty,
67) -> Result<i32, CommandSyntaxError> {
68    let domain = context.source().world().domain();
69    let worlds = context.source().server().worlds.worlds_in_domain(domain);
70    if worlds.iter().all(|world| world.difficulty() == difficulty) {
71        return Err(CommandSyntaxError::dynamic(
72            translations::COMMANDS_DIFFICULTY_FAILURE
73                .message([TextComponent::from(difficulty_display_name(difficulty))])
74                .component(),
75        ));
76    }
77
78    for world in worlds {
79        world.set_difficulty(difficulty);
80    }
81
82    let message = translations::COMMANDS_DIFFICULTY_SUCCESS
83        .message([TextComponent::from(difficulty_display_name(difficulty))])
84        .component();
85    context.source().send_success(&message, true);
86    Ok(0)
87}
88
89const fn difficulty_display_name(difficulty: Difficulty) -> &'static Translation<0> {
90    match difficulty {
91        Difficulty::Peaceful => &translations::OPTIONS_DIFFICULTY_PEACEFUL,
92        Difficulty::Easy => &translations::OPTIONS_DIFFICULTY_EASY,
93        Difficulty::Normal => &translations::OPTIONS_DIFFICULTY_NORMAL,
94        Difficulty::Hard => &translations::OPTIONS_DIFFICULTY_HARD,
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::difficulty_permission;
101    use crate::permission::{PermissionEntry, PermissionKey, PermissionSet};
102
103    #[test]
104    fn client_difficulty_permission_uses_the_command_root() {
105        let permission = difficulty_permission().expect("permission should build");
106        let allowed = PermissionSet::from_entries([PermissionEntry::allow(
107            PermissionKey::parse("minecraft.command.difficulty")
108                .expect("test permission should parse"),
109        )]);
110        let gamemode_only = PermissionSet::from_entries([PermissionEntry::allow(
111            PermissionKey::parse("minecraft.command.gamemode")
112                .expect("test permission should parse"),
113        )]);
114
115        assert!(allowed.allows(&permission));
116        assert!(!gamemode_only.allows(&permission));
117    }
118}