steel_core/command/builtins/
gamerule.rs1use 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 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}