1use std::sync::Arc;
4
5use steel_registry::{
6 blocks::block_state_ext::BlockStateExt as _, vanilla_blocks,
7 vanilla_game_rules::MAX_BLOCK_MODIFICATIONS,
8};
9use steel_utils::{BlockPos, BoundingBox, Identifier, translations};
10use text_components::TextComponent;
11
12use super::super::{
13 brigadier::{CommandNodeBuilder, CommandSyntaxError},
14 execution::{
15 BlockInput, BlockPredicate, CommandSource, SteelArgumentType, SteelCommandContext,
16 SteelCommandRuntime, argument, literal, placement_flags,
17 },
18 registration::CommandRegistration,
19};
20use super::execute::{ensure_region_chunks_block_ticking, loaded_block_position};
21use crate::world::World;
22
23type Builder = CommandNodeBuilder<CommandSource, SteelCommandRuntime>;
24
25#[derive(Clone, Copy)]
26enum FillMode {
27 Replace,
28 Outline,
29 Hollow,
30 Destroy,
31}
32
33#[derive(Clone, Copy)]
34enum FilterArgument {
35 All,
36 Keep,
37 Predicate(&'static str),
38}
39
40enum FillSelection<'predicate> {
41 All,
42 Keep,
43 Predicate(&'predicate BlockPredicate),
44}
45
46impl FillSelection<'_> {
47 fn matches(&self, world: &World, pos: BlockPos) -> bool {
48 match self {
49 Self::All => true,
50 Self::Keep => world.get_block_state(pos).is_air(),
51 Self::Predicate(predicate) => predicate.matches(world, pos),
52 }
53 }
54}
55
56pub(super) fn registration() -> CommandRegistration<CommandSource> {
57 CommandRegistration::new(Identifier::vanilla_static("fill"), |_| command())
58}
59
60fn command() -> Builder {
61 let block =
62 with_modes(
63 argument("block", SteelArgumentType::block_state()),
64 FilterArgument::All,
65 )
66 .then(
67 literal("replace")
68 .executes(|context| {
69 execute_fill(context, FillMode::Replace, FilterArgument::All, false)
70 })
71 .then(with_modes(
72 argument("filter", SteelArgumentType::block_predicate()),
73 FilterArgument::Predicate("filter"),
74 )),
75 )
76 .then(literal("keep").executes(|context| {
77 execute_fill(context, FillMode::Replace, FilterArgument::Keep, false)
78 }));
79
80 literal("fill").then(
81 argument("from", SteelArgumentType::block_pos())
82 .then(argument("to", SteelArgumentType::block_pos()).then(block)),
83 )
84}
85
86fn with_modes(builder: Builder, filter: FilterArgument) -> Builder {
87 builder
88 .executes(move |context| execute_fill(context, FillMode::Replace, filter, false))
89 .then(
90 literal("outline")
91 .executes(move |context| execute_fill(context, FillMode::Outline, filter, false)),
92 )
93 .then(
94 literal("hollow")
95 .executes(move |context| execute_fill(context, FillMode::Hollow, filter, false)),
96 )
97 .then(
98 literal("destroy")
99 .executes(move |context| execute_fill(context, FillMode::Destroy, filter, false)),
100 )
101 .then(
102 literal("strict")
103 .executes(move |context| execute_fill(context, FillMode::Replace, filter, true)),
104 )
105}
106
107fn execute_fill(
108 context: &SteelCommandContext<CommandSource>,
109 mode: FillMode,
110 filter: FilterArgument,
111 strict: bool,
112) -> Result<i32, CommandSyntaxError> {
113 let from = loaded_block_position(context, "from")?;
114 let to = loaded_block_position(context, "to")?;
115 let target = context.block_input("block")?;
116 let selection = match filter {
117 FilterArgument::All => FillSelection::All,
118 FilterArgument::Keep => FillSelection::Keep,
119 FilterArgument::Predicate(name) => FillSelection::Predicate(context.block_predicate(name)?),
120 };
121 let count = fill_blocks(
122 context.source().world(),
123 BoundingBox::from_corners(from, to),
124 target,
125 mode,
126 selection,
127 strict,
128 )?;
129
130 let message = translations::COMMANDS_FILL_SUCCESS
131 .message([TextComponent::from(count.to_string())])
132 .component();
133 context.source().send_success(&message, true);
134 Ok(count)
135}
136
137fn fill_blocks(
138 world: &Arc<World>,
139 region: BoundingBox,
140 target: &BlockInput,
141 mode: FillMode,
142 selection: FillSelection<'_>,
143 strict: bool,
144) -> Result<i32, CommandSyntaxError> {
145 let area = block_region_volume(region);
146 let limit = world.get_game_rule(&MAX_BLOCK_MODIFICATIONS);
147 if area > i64::from(limit) {
148 return Err(area_too_large(limit, area));
149 }
150 ensure_region_chunks_block_ticking(world, ®ion)?;
154
155 let air = BlockInput::from_state(vanilla_blocks::AIR.default_state());
156 let flags = placement_flags(strict);
157 let mut updated_positions = Vec::new();
158 let mut count = 0;
159
160 for z in region.min_z()..=region.max_z() {
161 for y in region.min_y()..=region.max_y() {
162 for x in region.min_x()..=region.max_x() {
163 let pos = BlockPos::new(x, y, z);
164 if !selection.matches(world, pos) {
165 continue;
166 }
167
168 let old_state = world.get_block_state(pos);
169 let affected = matches!(mode, FillMode::Destroy) && world.destroy_block(pos, true);
170 let input = input_at(mode, region, pos, target, &air);
171 let Some(input) = input else {
172 if affected {
173 count += 1;
174 }
175 continue;
176 };
177
178 let placed = input.place(world, pos, flags)?;
179 if !placed {
180 if affected {
181 count += 1;
182 }
183 continue;
184 }
185
186 if !strict {
187 updated_positions.push((pos, old_state));
188 }
189 count += 1;
190 }
191 }
192 }
193
194 for (pos, old_state) in updated_positions {
195 world.update_neighbors_on_block_set(pos, old_state);
196 }
197
198 if count == 0 {
199 return Err(CommandSyntaxError::dynamic(TextComponent::from(
200 &translations::COMMANDS_FILL_FAILED,
201 )));
202 }
203 Ok(count)
204}
205
206const fn input_at<'input>(
207 mode: FillMode,
208 region: BoundingBox,
209 pos: BlockPos,
210 target: &'input BlockInput,
211 air: &'input BlockInput,
212) -> Option<&'input BlockInput> {
213 let boundary = pos.x() == region.min_x()
214 || pos.x() == region.max_x()
215 || pos.y() == region.min_y()
216 || pos.y() == region.max_y()
217 || pos.z() == region.min_z()
218 || pos.z() == region.max_z();
219 match mode {
220 FillMode::Outline if !boundary => None,
221 FillMode::Hollow if !boundary => Some(air),
222 FillMode::Replace | FillMode::Outline | FillMode::Hollow | FillMode::Destroy => {
223 Some(target)
224 }
225 }
226}
227
228fn block_region_volume(region: BoundingBox) -> i64 {
229 let x_span = i64::from(region.max_x()) - i64::from(region.min_x()) + 1;
230 let y_span = i64::from(region.max_y()) - i64::from(region.min_y()) + 1;
231 let z_span = i64::from(region.max_z()) - i64::from(region.min_z()) + 1;
232 x_span.saturating_mul(y_span).saturating_mul(z_span)
233}
234
235fn area_too_large(limit: i32, area: i64) -> CommandSyntaxError {
236 let message = translations::COMMANDS_FILL_TOOBIG
237 .message([
238 TextComponent::from(limit.to_string()),
239 TextComponent::from(area.to_string()),
240 ])
241 .component();
242 CommandSyntaxError::dynamic(message)
243}
244
245#[cfg(test)]
246mod tests {
247 use steel_registry::{init_vanilla_registry, vanilla_game_rules};
248 use steel_utils::{ChunkPos, Downcast as _, WorldAabb, types::UpdateFlags};
249
250 use super::super::create_dispatcher;
251 use super::*;
252 use crate::{
253 behavior::init_behaviors,
254 block_entity::init_block_entities,
255 command::{
256 brigadier::{CommandDispatcher, NodeId},
257 execution::{SteelArgumentType, SteelCommandRuntime},
258 },
259 entity::entities::ItemEntity,
260 test_support::{
261 TestWorld, fresh_test_world, insert_ready_full_chunk, insert_unready_full_chunk,
262 },
263 };
264
265 type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
266
267 fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
268 let Some(children) = dispatcher.children(parent) else {
269 panic!("parent node should exist");
270 };
271 let Some(child) = children.iter().copied().find(|child| {
272 dispatcher
273 .node(*child)
274 .is_some_and(|node| node.name() == name)
275 }) else {
276 panic!("child {name} should exist");
277 };
278 child
279 }
280
281 fn assert_executable(dispatcher: &Dispatcher, node: NodeId) {
282 let Some(node) = dispatcher.node(node) else {
283 panic!("command node should exist");
284 };
285 assert!(node.is_executable());
286 }
287
288 fn setup_world(key: &'static str, chunk: ChunkPos) -> TestWorld {
289 init_vanilla_registry();
290 init_behaviors();
291 init_block_entities();
292 let world_fixture = fresh_test_world(key);
293 let world = &world_fixture.world;
294 insert_ready_full_chunk(world, chunk);
295 world_fixture
296 }
297
298 #[test]
299 fn fill_graph_exposes_all_vanilla_modes_and_typed_arguments() {
300 init_vanilla_registry();
301
302 let Ok(dispatcher) = create_dispatcher() else {
303 panic!("built-in commands should register");
304 };
305 let fill = child(&dispatcher, dispatcher.root(), "fill");
306 let from = child(&dispatcher, fill, "from");
307 let to = child(&dispatcher, from, "to");
308 let block = child(&dispatcher, to, "block");
309 assert_eq!(
310 dispatcher.node(block).and_then(|node| node.argument_type()),
311 Some(&SteelArgumentType::block_state())
312 );
313 assert_executable(&dispatcher, block);
314
315 for mode in ["outline", "hollow", "destroy", "strict", "keep"] {
316 let node = child(&dispatcher, block, mode);
317 assert_executable(&dispatcher, node);
318 }
319 let replace = child(&dispatcher, block, "replace");
320 assert_executable(&dispatcher, replace);
321 let filter = child(&dispatcher, replace, "filter");
322 assert_eq!(
323 dispatcher
324 .node(filter)
325 .and_then(|node| node.argument_type()),
326 Some(&SteelArgumentType::block_predicate())
327 );
328 for mode in ["outline", "hollow", "destroy", "strict"] {
329 let node = child(&dispatcher, filter, mode);
330 assert_executable(&dispatcher, node);
331 }
332 }
333
334 #[test]
335 fn hollow_replaces_the_shell_and_clears_the_core() {
336 let origin = BlockPos::new(4, 64, 4);
337 let world_fixture = setup_world("fill_hollow", ChunkPos::from_block_pos(origin));
338 let world = &world_fixture.world;
339 let region = BoundingBox::from_corners(origin, origin.offset(2, 2, 2));
340 for z in region.min_z()..=region.max_z() {
341 for y in region.min_y()..=region.max_y() {
342 for x in region.min_x()..=region.max_x() {
343 assert!(world.set_block(
344 BlockPos::new(x, y, z),
345 vanilla_blocks::STONE.default_state(),
346 UpdateFlags::UPDATE_NONE,
347 ));
348 }
349 }
350 }
351
352 let target = BlockInput::from_state(vanilla_blocks::GLASS.default_state());
353 let result = fill_blocks(
354 world,
355 region,
356 &target,
357 FillMode::Hollow,
358 FillSelection::All,
359 false,
360 );
361
362 assert_eq!(result, Ok(27));
363 assert!(world.get_block_state(origin.offset(1, 1, 1)).is_air());
364 assert_eq!(
365 world.get_block_state(origin).get_block(),
366 &vanilla_blocks::GLASS
367 );
368 }
369
370 #[test]
371 fn replace_filter_only_changes_matching_blocks() {
372 let origin = BlockPos::new(5, 64, 5);
373 let world_fixture = setup_world("fill_filter", ChunkPos::from_block_pos(origin));
374 let world = &world_fixture.world;
375 let states = [
376 vanilla_blocks::STONE.default_state(),
377 vanilla_blocks::DIRT.default_state(),
378 vanilla_blocks::STONE.default_state(),
379 ];
380 for (offset, state) in states.into_iter().enumerate() {
381 assert!(world.set_block(
382 origin.offset(offset as i32, 0, 0),
383 state,
384 UpdateFlags::UPDATE_NONE,
385 ));
386 }
387 let predicate = BlockPredicate::Block {
388 block: &vanilla_blocks::STONE,
389 properties: Vec::new(),
390 nbt: None,
391 };
392 let target = BlockInput::from_state(vanilla_blocks::GLASS.default_state());
393
394 let result = fill_blocks(
395 world,
396 BoundingBox::from_corners(origin, origin.offset(2, 0, 0)),
397 &target,
398 FillMode::Replace,
399 FillSelection::Predicate(&predicate),
400 false,
401 );
402
403 assert_eq!(result, Ok(2));
404 assert_eq!(
405 world.get_block_state(origin).get_block(),
406 &vanilla_blocks::GLASS
407 );
408 assert_eq!(
409 world.get_block_state(origin.east()).get_block(),
410 &vanilla_blocks::DIRT
411 );
412 assert_eq!(
413 world.get_block_state(origin.east().east()).get_block(),
414 &vanilla_blocks::GLASS
415 );
416 }
417
418 #[test]
419 fn destroy_mode_counts_the_destroyed_block_and_drops_its_loot() {
420 let pos = BlockPos::new(8, 64, 8);
421 let world_fixture = setup_world("fill_destroy", ChunkPos::from_block_pos(pos));
422 let world = &world_fixture.world;
423 assert!(world.set_block(
424 pos,
425 vanilla_blocks::DIRT.default_state(),
426 UpdateFlags::UPDATE_NONE,
427 ));
428 let air = BlockInput::from_state(vanilla_blocks::AIR.default_state());
429
430 assert_eq!(
431 fill_blocks(
432 world,
433 BoundingBox::from_corners(pos, pos),
434 &air,
435 FillMode::Destroy,
436 FillSelection::All,
437 false,
438 ),
439 Ok(1)
440 );
441
442 assert!(world.get_block_state(pos).is_air());
443 assert!(
444 world
445 .get_entities_in_aabb(&WorldAabb::new(7.0, 63.0, 7.0, 10.0, 67.0, 10.0))
446 .iter()
447 .any(|entity| entity.downcast_ref::<ItemEntity>().is_some())
448 );
449 }
450
451 #[test]
452 fn fill_limit_and_unloaded_region_fail_before_mutation() {
453 let first = BlockPos::new(15, 64, 0);
454 let world_fixture = setup_world("fill_preflight", ChunkPos::from_block_pos(first));
455 let world = &world_fixture.world;
456 assert!(world.set_game_rule(&vanilla_game_rules::MAX_BLOCK_MODIFICATIONS, 1));
457 let target = BlockInput::from_state(vanilla_blocks::STONE.default_state());
458 let two_blocks = BoundingBox::from_corners(first, first.east());
459 assert!(
460 fill_blocks(
461 world,
462 two_blocks,
463 &target,
464 FillMode::Replace,
465 FillSelection::All,
466 false,
467 )
468 .is_err()
469 );
470 assert!(world.get_block_state(first).is_air());
471
472 assert!(world.set_game_rule(&vanilla_game_rules::MAX_BLOCK_MODIFICATIONS, 32_768));
473 assert!(
474 fill_blocks(
475 world,
476 two_blocks,
477 &target,
478 FillMode::Replace,
479 FillSelection::All,
480 false,
481 )
482 .is_err()
483 );
484 assert!(world.get_block_state(first).is_air());
485 }
486
487 #[test]
488 fn fill_requires_block_ticking_readiness_for_its_neighbor_halo() {
489 let pos = BlockPos::new(15, 64, 8);
490 init_vanilla_registry();
491 init_behaviors();
492 init_block_entities();
493
494 let unavailable_fixture = fresh_test_world("fill_unready_halo");
495 let unavailable = &unavailable_fixture.world;
496 insert_unready_full_chunk(unavailable, ChunkPos::new(0, 0));
497 insert_ready_full_chunk(unavailable, ChunkPos::new(1, 0));
498 let fence = BlockInput::from_state(vanilla_blocks::OAK_FENCE.default_state());
499 assert!(
500 fill_blocks(
501 unavailable,
502 BoundingBox::from_corners(pos, pos),
503 &fence,
504 FillMode::Replace,
505 FillSelection::All,
506 false,
507 )
508 .is_err()
509 );
510 assert!(unavailable.get_block_state(pos).is_air());
511
512 let ready_fixture = fresh_test_world("fill_ready_halo");
513 let ready = &ready_fixture.world;
514 insert_ready_full_chunk(ready, ChunkPos::new(0, 0));
515 insert_ready_full_chunk(ready, ChunkPos::new(1, 0));
516 assert!(ready.set_block(
517 pos.east(),
518 vanilla_blocks::OAK_FENCE.default_state(),
519 UpdateFlags::UPDATE_NONE,
520 ));
521 assert_eq!(
522 fill_blocks(
523 ready,
524 BoundingBox::from_corners(pos, pos),
525 &fence,
526 FillMode::Replace,
527 FillSelection::All,
528 false,
529 ),
530 Ok(1)
531 );
532 assert!(
533 steel_registry::REGISTRY
534 .blocks
535 .properties(ready.get_block_state(pos))
536 .any(|property| property == ("east", "true"))
537 );
538 }
539}