Skip to main content

steel_core/command/builtins/
gamerule.rs

1use steel_registry::{
2    REGISTRY,
3    game_rules::{ErasedGameRuleRef, GameRuleType, GameRuleValue},
4};
5use steel_utils::{Identifier, translations};
6use text_components::TextComponent;
7
8use super::super::{
9    brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
10    execution::{CommandSource, SteelCommandContext, SteelCommandRuntime, argument, literal},
11    registration::CommandRegistration,
12};
13
14pub(super) fn registration() -> CommandRegistration<CommandSource> {
15    CommandRegistration::new(Identifier::vanilla_static("gamerule"), |_| command())
16}
17
18fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
19    let mut command = literal("gamerule");
20    for (_, rule) in REGISTRY.game_rules.iter() {
21        // Vanilla's short identifier only omits the `minecraft` namespace.
22        if rule.key().namespace == Identifier::VANILLA_NAMESPACE {
23            command = command.then(rule_literal(rule.key().path.to_string(), rule));
24        }
25        command = command.then(rule_literal(rule.key().to_string(), rule));
26    }
27    command
28}
29
30fn rule_literal(
31    name: String,
32    rule: ErasedGameRuleRef,
33) -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
34    match rule.value_type() {
35        GameRuleType::Bool => literal(name)
36            .executes(move |context| query_rule(context, rule))
37            .then(
38                argument("value", ArgumentType::bool())
39                    .executes(move |context| set_bool_rule(context, rule)),
40            ),
41        GameRuleType::Int => {
42            let minimum = rule.min_value().unwrap_or(i32::MIN);
43            let maximum = rule.max_value().unwrap_or(i32::MAX);
44            literal(name)
45                .executes(move |context| query_rule(context, rule))
46                .then(
47                    argument("value", ArgumentType::integer(minimum, maximum))
48                        .executes(move |context| set_int_rule(context, rule)),
49                )
50        }
51    }
52}
53
54#[expect(
55    clippy::unnecessary_wraps,
56    reason = "Command executors use a shared fallible callback signature."
57)]
58fn query_rule(
59    context: &SteelCommandContext<CommandSource>,
60    rule: ErasedGameRuleRef,
61) -> Result<i32, CommandSyntaxError> {
62    let value = context.source().world().get_erased_game_rule(rule);
63    let message = translations::COMMANDS_GAMERULE_QUERY
64        .message([
65            TextComponent::from(rule_display_name(rule)),
66            TextComponent::from(value.to_string()),
67        ])
68        .component();
69    context.source().send_success(&message, false);
70    Ok(rule.erased_command_result(&value))
71}
72
73fn set_bool_rule(
74    context: &SteelCommandContext<CommandSource>,
75    rule: ErasedGameRuleRef,
76) -> Result<i32, CommandSyntaxError> {
77    let Some(value) = context.boolean("value") else {
78        return Err(missing_rule_value(rule));
79    };
80    set_rule(context, rule, GameRuleValue::new(value))
81}
82
83fn set_int_rule(
84    context: &SteelCommandContext<CommandSource>,
85    rule: ErasedGameRuleRef,
86) -> Result<i32, CommandSyntaxError> {
87    let Some(value) = context.integer("value") else {
88        return Err(missing_rule_value(rule));
89    };
90    set_rule(context, rule, GameRuleValue::new(value))
91}
92
93fn set_rule(
94    context: &SteelCommandContext<CommandSource>,
95    rule: ErasedGameRuleRef,
96    value: GameRuleValue,
97) -> Result<i32, CommandSyntaxError> {
98    let result = rule.erased_command_result(&value);
99    let serialized_value = value.to_string();
100    if !context.source().world().set_erased_game_rule(rule, value) {
101        return Err(CommandSyntaxError::dynamic(format!(
102            "Parsed value does not match game rule {}",
103            rule.key()
104        )));
105    }
106
107    let message = translations::COMMANDS_GAMERULE_SET
108        .message([
109            TextComponent::from(rule_display_name(rule)),
110            TextComponent::from(serialized_value),
111        ])
112        .component();
113    context.source().send_success(&message, true);
114    Ok(result)
115}
116
117fn missing_rule_value(rule: ErasedGameRuleRef) -> CommandSyntaxError {
118    CommandSyntaxError::dynamic(format!(
119        "Parsed value for game rule {} is missing from the command context",
120        rule.key()
121    ))
122}
123
124fn rule_display_name(rule: ErasedGameRuleRef) -> String {
125    if rule.key().namespace == Identifier::VANILLA_NAMESPACE {
126        rule.key().path.to_string()
127    } else {
128        rule.key().to_string()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::super::create_dispatcher;
135    use crate::command::{
136        brigadier::{ArgumentType, CommandDispatcher, NodeId},
137        execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
138    };
139    use steel_registry::init_vanilla_registry;
140
141    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
142
143    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
144        let Some(children) = dispatcher.children(parent) else {
145            panic!("parent node should exist");
146        };
147        let Some(child) = children.iter().copied().find(|child| {
148            dispatcher
149                .node(*child)
150                .is_some_and(|node| node.name() == name)
151        }) else {
152            panic!("child `{name}` should exist");
153        };
154        child
155    }
156
157    #[test]
158    fn vanilla_rules_have_short_and_qualified_literals() {
159        init_vanilla_registry();
160        let Ok(dispatcher) = create_dispatcher() else {
161            panic!("built-in commands should register");
162        };
163        let gamerule = child(&dispatcher, dispatcher.root(), "gamerule");
164        let short = child(&dispatcher, gamerule, "keep_inventory");
165        let qualified = child(&dispatcher, gamerule, "minecraft:keep_inventory");
166
167        for rule in [short, qualified] {
168            let Some(rule_node) = dispatcher.node(rule) else {
169                panic!("gamerule literal should exist");
170            };
171            assert!(rule_node.is_executable());
172            let value = child(&dispatcher, rule, "value");
173            assert_eq!(
174                dispatcher.node(value).and_then(|node| node.argument_type()),
175                Some(&SteelArgumentType::from(ArgumentType::bool()))
176            );
177        }
178    }
179
180    #[test]
181    fn integer_rule_bounds_are_retained_in_the_graph() {
182        init_vanilla_registry();
183        let Ok(dispatcher) = create_dispatcher() else {
184            panic!("built-in commands should register");
185        };
186        let gamerule = child(&dispatcher, dispatcher.root(), "gamerule");
187        let rule = child(&dispatcher, gamerule, "max_command_forks");
188        let value = child(&dispatcher, rule, "value");
189
190        assert_eq!(
191            dispatcher.node(value).and_then(|node| node.argument_type()),
192            Some(&SteelArgumentType::from(ArgumentType::integer(0, i32::MAX)))
193        );
194    }
195}