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 value = context.boolean("value")?;
78    set_rule(context, rule, GameRuleValue::new(value))
79}
80
81fn set_int_rule(
82    context: &SteelCommandContext<CommandSource>,
83    rule: ErasedGameRuleRef,
84) -> Result<i32, CommandSyntaxError> {
85    let value = context.integer("value")?;
86    set_rule(context, rule, GameRuleValue::new(value))
87}
88
89fn set_rule(
90    context: &SteelCommandContext<CommandSource>,
91    rule: ErasedGameRuleRef,
92    value: GameRuleValue,
93) -> Result<i32, CommandSyntaxError> {
94    let result = rule.erased_command_result(&value);
95    let serialized_value = value.to_string();
96    if !context.source().world().set_erased_game_rule(rule, value) {
97        return Err(CommandSyntaxError::dynamic(format!(
98            "Parsed value does not match game rule {}",
99            rule.key()
100        )));
101    }
102
103    let message = translations::COMMANDS_GAMERULE_SET
104        .message([
105            TextComponent::from(rule_display_name(rule)),
106            TextComponent::from(serialized_value),
107        ])
108        .component();
109    context.source().send_success(&message, true);
110    Ok(result)
111}
112
113fn rule_display_name(rule: ErasedGameRuleRef) -> String {
114    if rule.key().namespace == Identifier::VANILLA_NAMESPACE {
115        rule.key().path.to_string()
116    } else {
117        rule.key().to_string()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::super::create_dispatcher;
124    use crate::command::{
125        brigadier::{ArgumentType, CommandDispatcher, NodeId},
126        execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
127    };
128    use steel_registry::init_vanilla_registry;
129
130    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
131
132    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
133        let Some(children) = dispatcher.children(parent) else {
134            panic!("parent node should exist");
135        };
136        let Some(child) = children.iter().copied().find(|child| {
137            dispatcher
138                .node(*child)
139                .is_some_and(|node| node.name() == name)
140        }) else {
141            panic!("child `{name}` should exist");
142        };
143        child
144    }
145
146    #[test]
147    fn vanilla_rules_have_short_and_qualified_literals() {
148        init_vanilla_registry();
149        let Ok(dispatcher) = create_dispatcher() else {
150            panic!("built-in commands should register");
151        };
152        let gamerule = child(&dispatcher, dispatcher.root(), "gamerule");
153        let short = child(&dispatcher, gamerule, "keep_inventory");
154        let qualified = child(&dispatcher, gamerule, "minecraft:keep_inventory");
155
156        for rule in [short, qualified] {
157            let Some(rule_node) = dispatcher.node(rule) else {
158                panic!("gamerule literal should exist");
159            };
160            assert!(rule_node.is_executable());
161            let value = child(&dispatcher, rule, "value");
162            assert_eq!(
163                dispatcher.node(value).and_then(|node| node.argument_type()),
164                Some(&SteelArgumentType::from(ArgumentType::bool()))
165            );
166        }
167    }
168
169    #[test]
170    fn integer_rule_bounds_are_retained_in_the_graph() {
171        init_vanilla_registry();
172        let Ok(dispatcher) = create_dispatcher() else {
173            panic!("built-in commands should register");
174        };
175        let gamerule = child(&dispatcher, dispatcher.root(), "gamerule");
176        let rule = child(&dispatcher, gamerule, "max_command_forks");
177        let value = child(&dispatcher, rule, "value");
178
179        assert_eq!(
180            dispatcher.node(value).and_then(|node| node.argument_type()),
181            Some(&SteelArgumentType::from(ArgumentType::integer(0, i32::MAX)))
182        );
183    }
184}