steel_core/command/builtins/
worldborder.rs1use steel_utils::{Identifier, translations};
4use text_components::{TextComponent, translation::Translation};
5
6use super::super::{
7 brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
8 execution::{
9 CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
10 literal,
11 },
12 registration::CommandRegistration,
13};
14use crate::world::{MAX_CENTER_COORDINATE, MAX_SIZE, WorldBorderError};
15
16const MIN_SIZE: f64 = 1.0;
17
18pub(super) fn registration() -> CommandRegistration<CommandSource> {
19 CommandRegistration::new(Identifier::vanilla_static("worldborder"), |_| command())
20}
21
22fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
23 literal("worldborder")
24 .then(size_command("add", add_size))
25 .then(size_command("set", set_requested_size))
26 .then(
27 literal("center")
28 .then(argument("pos", SteelArgumentType::vec2(true)).executes(set_center)),
29 )
30 .then(
31 literal("damage")
32 .then(
33 literal("amount").then(
34 argument("damagePerBlock", ArgumentType::float(0.0, f32::MAX))
35 .executes(set_damage_amount),
36 ),
37 )
38 .then(
39 literal("buffer").then(
40 argument("distance", ArgumentType::float(0.0, f32::MAX))
41 .executes(set_damage_buffer),
42 ),
43 ),
44 )
45 .then(literal("get").executes(get_size))
46 .then(
47 literal("warning")
48 .then(
49 literal("distance").then(
50 argument("distance", ArgumentType::integer(0, i32::MAX))
51 .executes(set_warning_distance),
52 ),
53 )
54 .then(
55 literal("time").then(
56 argument("time", SteelArgumentType::time(0)).executes(set_warning_time),
57 ),
58 ),
59 )
60}
61
62fn size_command(
63 name: &'static str,
64 execute: fn(
65 &SteelCommandContext<CommandSource>,
66 Option<i64>,
67 ) -> Result<i32, CommandSyntaxError>,
68) -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
69 literal(name).then(
70 argument("distance", ArgumentType::double(-MAX_SIZE, MAX_SIZE))
71 .executes(move |context| execute(context, None))
72 .then(
73 argument("time", SteelArgumentType::time(0)).executes(move |context| {
74 let ticks = i64::from(context.time("time")?);
75 execute(context, Some(ticks))
76 }),
77 ),
78 )
79}
80
81fn add_size(
82 context: &SteelCommandContext<CommandSource>,
83 requested_ticks: Option<i64>,
84) -> Result<i32, CommandSyntaxError> {
85 let border = context.source().world().world_border_snapshot();
86 let distance = border.old_size + context.double("distance")?;
87 let ticks = added_lerp_time(border.lerp_time, requested_ticks);
88 set_size(context, distance, ticks)
89}
90
91const fn added_lerp_time(current_ticks: i64, requested_ticks: Option<i64>) -> i64 {
92 match requested_ticks {
93 Some(ticks) => current_ticks.wrapping_add(ticks),
94 None => 0,
95 }
96}
97
98fn set_requested_size(
99 context: &SteelCommandContext<CommandSource>,
100 ticks: Option<i64>,
101) -> Result<i32, CommandSyntaxError> {
102 set_size(context, context.double("distance")?, ticks.unwrap_or(0))
103}
104
105#[expect(
106 clippy::float_cmp,
107 reason = "Vanilla rejects only exactly unchanged border sizes."
108)]
109fn set_size(
110 context: &SteelCommandContext<CommandSource>,
111 distance: f64,
112 ticks: i64,
113) -> Result<i32, CommandSyntaxError> {
114 let world = context.source().world();
115 let current = world.world_border_snapshot().old_size;
116 if current == distance {
117 return Err(translated_error(
118 &translations::COMMANDS_WORLDBORDER_SET_FAILED_NOCHANGE,
119 ));
120 }
121 if distance < MIN_SIZE {
122 return Err(translated_error(
123 &translations::COMMANDS_WORLDBORDER_SET_FAILED_SMALL,
124 ));
125 }
126 if distance > MAX_SIZE {
127 let message = translations::COMMANDS_WORLDBORDER_SET_FAILED_BIG
128 .message([format!("{MAX_SIZE:.6E}")])
129 .component();
130 return Err(CommandSyntaxError::dynamic(message));
131 }
132
133 let formatted_distance = format!("{distance:.1}");
134 let message = if ticks > 0 {
135 world
136 .lerp_world_border_size_between(current, distance, ticks)
137 .map_err(internal_border_error)?;
138 let seconds = format_ticks_to_seconds(ticks);
139 if distance > current {
140 translations::COMMANDS_WORLDBORDER_SET_GROW
141 .message([formatted_distance, seconds])
142 .component()
143 } else {
144 translations::COMMANDS_WORLDBORDER_SET_SHRINK
145 .message([formatted_distance, seconds])
146 .component()
147 }
148 } else {
149 world
150 .set_world_border_size(distance)
151 .map_err(internal_border_error)?;
152 translations::COMMANDS_WORLDBORDER_SET_IMMEDIATE
153 .message([formatted_distance])
154 .component()
155 };
156 context.source().send_success(&message, true);
157 Ok((distance - current) as i32)
158}
159
160#[expect(
161 clippy::cast_possible_truncation,
162 clippy::float_cmp,
163 reason = "Vanilla resolves Vec2 components as f32 and compares exact center values."
164)]
165fn set_center(context: &SteelCommandContext<CommandSource>) -> Result<i32, CommandSyntaxError> {
166 let position = context.coordinates("pos")?.position(context.source());
167 let center_x = f64::from(position.x as f32);
168 let center_z = f64::from(position.z as f32);
169 let world = context.source().world();
170 let border = world.world_border_snapshot();
171 if border.center_x == center_x && border.center_z == center_z {
172 return Err(translated_error(
173 &translations::COMMANDS_WORLDBORDER_CENTER_FAILED,
174 ));
175 }
176 if center_x.abs() > MAX_CENTER_COORDINATE || center_z.abs() > MAX_CENTER_COORDINATE {
177 let message = translations::COMMANDS_WORLDBORDER_SET_FAILED_FAR
178 .message([format!("{MAX_CENTER_COORDINATE:.7E}")])
179 .component();
180 return Err(CommandSyntaxError::dynamic(message));
181 }
182
183 world
184 .set_world_border_center(center_x, center_z)
185 .map_err(internal_border_error)?;
186 let message = translations::COMMANDS_WORLDBORDER_CENTER_SUCCESS
187 .message([format!("{center_x:.2}"), format!("{center_z:.2}")])
188 .component();
189 context.source().send_success(&message, true);
190 Ok(0)
191}
192
193#[expect(
194 clippy::float_cmp,
195 reason = "Vanilla rejects only an exactly unchanged damage buffer."
196)]
197fn set_damage_buffer(
198 context: &SteelCommandContext<CommandSource>,
199) -> Result<i32, CommandSyntaxError> {
200 let distance = context.float("distance")?;
201 let world = context.source().world();
202 if world.world_border_snapshot().safe_zone == f64::from(distance) {
203 return Err(translated_error(
204 &translations::COMMANDS_WORLDBORDER_DAMAGE_BUFFER_FAILED,
205 ));
206 }
207 world
208 .set_world_border_safe_zone(f64::from(distance))
209 .map_err(internal_border_error)?;
210 let message = translations::COMMANDS_WORLDBORDER_DAMAGE_BUFFER_SUCCESS
211 .message([format!("{distance:.2}")])
212 .component();
213 context.source().send_success(&message, true);
214 Ok(distance as i32)
215}
216
217#[expect(
218 clippy::float_cmp,
219 reason = "Vanilla rejects only an exactly unchanged damage amount."
220)]
221fn set_damage_amount(
222 context: &SteelCommandContext<CommandSource>,
223) -> Result<i32, CommandSyntaxError> {
224 let damage = context.float("damagePerBlock")?;
225 let world = context.source().world();
226 if world.world_border_snapshot().damage_per_block == f64::from(damage) {
227 return Err(translated_error(
228 &translations::COMMANDS_WORLDBORDER_DAMAGE_AMOUNT_FAILED,
229 ));
230 }
231 world
232 .set_world_border_damage_per_block(f64::from(damage))
233 .map_err(internal_border_error)?;
234 let message = translations::COMMANDS_WORLDBORDER_DAMAGE_AMOUNT_SUCCESS
235 .message([format!("{damage:.2}")])
236 .component();
237 context.source().send_success(&message, true);
238 Ok(damage as i32)
239}
240
241fn set_warning_time(
242 context: &SteelCommandContext<CommandSource>,
243) -> Result<i32, CommandSyntaxError> {
244 let ticks = context.time("time")?;
245 let world = context.source().world();
246 if world.world_border_snapshot().warning_time == ticks {
247 return Err(translated_error(
248 &translations::COMMANDS_WORLDBORDER_WARNING_TIME_FAILED,
249 ));
250 }
251 world.set_world_border_warning_time(ticks);
252 let message = translations::COMMANDS_WORLDBORDER_WARNING_TIME_SUCCESS
253 .message([format_ticks_to_seconds(i64::from(ticks))])
254 .component();
255 context.source().send_success(&message, true);
256 Ok(ticks)
257}
258
259fn set_warning_distance(
260 context: &SteelCommandContext<CommandSource>,
261) -> Result<i32, CommandSyntaxError> {
262 let distance = context.integer("distance")?;
263 let world = context.source().world();
264 if world.world_border_snapshot().warning_blocks == distance {
265 return Err(translated_error(
266 &translations::COMMANDS_WORLDBORDER_WARNING_DISTANCE_FAILED,
267 ));
268 }
269 world.set_world_border_warning_blocks(distance);
270 let message = translations::COMMANDS_WORLDBORDER_WARNING_DISTANCE_SUCCESS
271 .message([distance.to_string()])
272 .component();
273 context.source().send_success(&message, true);
274 Ok(distance)
275}
276
277#[expect(
278 clippy::cast_possible_truncation,
279 clippy::unnecessary_wraps,
280 reason = "Vanilla returns a signed result and command executors share a fallible signature."
281)]
282fn get_size(context: &SteelCommandContext<CommandSource>) -> Result<i32, CommandSyntaxError> {
283 let size = context.source().world().world_border_snapshot().old_size;
284 let message = translations::COMMANDS_WORLDBORDER_GET
285 .message([format!("{size:.0}")])
286 .component();
287 context.source().send_success(&message, false);
288 Ok((size + 0.5).floor() as i32)
289}
290
291fn format_ticks_to_seconds(ticks: i64) -> String {
292 format!("{:.2}", ticks as f64 / 20.0)
293}
294
295fn translated_error(translation: &Translation<0>) -> CommandSyntaxError {
296 CommandSyntaxError::dynamic(TextComponent::from(translation))
297}
298
299fn internal_border_error(error: WorldBorderError) -> CommandSyntaxError {
300 CommandSyntaxError::dynamic(TextComponent::from(error.to_string()))
301}
302
303#[cfg(test)]
304mod tests {
305 use super::{
306 MAX_CENTER_COORDINATE, MAX_SIZE, added_lerp_time, format_ticks_to_seconds, size_command,
307 };
308 use crate::command::{
309 brigadier::{ArgumentType, CommandDispatcher},
310 execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
311 };
312
313 #[test]
314 fn formats_ticks_as_vanilla_seconds() {
315 assert_eq!(format_ticks_to_seconds(0), "0.00");
316 assert_eq!(format_ticks_to_seconds(31), "1.55");
317 }
318
319 #[test]
320 fn formats_limits_like_vanilla_numeric_components() {
321 assert_eq!(format!("{MAX_SIZE:.6E}"), "5.999997E7");
322 assert_eq!(format!("{MAX_CENTER_COORDINATE:.7E}"), "2.9999984E7");
323 }
324
325 #[test]
326 fn add_preserves_existing_lerp_only_when_time_is_explicit() {
327 assert_eq!(added_lerp_time(40, None), 0);
328 assert_eq!(added_lerp_time(40, Some(0)), 40);
329 assert_eq!(added_lerp_time(40, Some(20)), 60);
330 }
331
332 #[test]
333 fn size_subcommands_accept_optional_time() {
334 let mut dispatcher = CommandDispatcher::<CommandSource, SteelCommandRuntime>::new();
335 let Ok(root) = dispatcher.register(size_command("set", |_, _| Ok(1))) else {
336 panic!("size command should register");
337 };
338 let Some(distance) = dispatcher
339 .children(root)
340 .and_then(|children| children.first())
341 else {
342 panic!("distance argument should exist");
343 };
344 assert_eq!(
345 dispatcher
346 .node(*distance)
347 .and_then(|node| node.argument_type()),
348 Some(&SteelArgumentType::from(ArgumentType::double(
349 -super::MAX_SIZE,
350 super::MAX_SIZE,
351 )))
352 );
353 let Some(time) = dispatcher
354 .children(*distance)
355 .and_then(|children| children.first())
356 else {
357 panic!("time argument should exist");
358 };
359 assert_eq!(
360 dispatcher.node(*time).and_then(|node| node.argument_type()),
361 Some(&SteelArgumentType::time(0))
362 );
363 }
364}