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 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}