Skip to main content

steel_core/command/builtins/
setblock.rs

1//! Vanilla set block command.
2
3use super::super::{
4    brigadier::{CommandNodeBuilder, CommandSyntaxError},
5    execution::{
6        CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
7        literal,
8    },
9    registration::CommandRegistration,
10};
11use crate::command::execution::BlockPredicate;
12use crate::world::tick_scheduler::TickPriority;
13use simdnbt::borrow::read_compound;
14use std::io::Cursor;
15use steel_registry::REGISTRY;
16use steel_registry::blocks::block_state_ext::BlockStateExt;
17use steel_utils::Identifier;
18use steel_utils::translations::{COMMANDS_SETBLOCK_FAILED, COMMANDS_SETBLOCK_SUCCESS};
19use steel_utils::types::UpdateFlags;
20use text_components::TextComponent;
21
22/// How the block should be placed
23enum SetBlockMode {
24    /// Destroy the previous block and drop the loot according to the loot table
25    Destroy,
26    /// Can only place a block if the previous block was air
27    Keep,
28    /// Base case
29    Replace,
30    /// Replace the block without updating the world
31    Strict,
32}
33
34pub(super) fn registration() -> CommandRegistration<CommandSource> {
35    CommandRegistration::new(Identifier::vanilla_static("setblock"), |_| command())
36}
37
38fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
39    literal("setblock").then(
40        argument("pos", SteelArgumentType::block_pos()).then(
41            argument("block", SteelArgumentType::block_state())
42                .executes(|c| set_block(c, SetBlockMode::Replace))
43                .then(literal("destroy").executes(|c| set_block(c, SetBlockMode::Destroy)))
44                .then(literal("keep").executes(|c| set_block(c, SetBlockMode::Keep)))
45                .then(literal("replace").executes(|c| set_block(c, SetBlockMode::Replace)))
46                .then(literal("strict").executes(|c| set_block(c, SetBlockMode::Strict))),
47        ),
48    )
49}
50
51/// Set a block in the desired position with a mode (destroy, keep, replace, strict), and return 1 if the block is placed, 0 else.
52fn set_block(
53    context: &SteelCommandContext<CommandSource>,
54    mode: SetBlockMode,
55) -> Result<i32, CommandSyntaxError> {
56    // Block pos
57    let Some(coordinates) = context.coordinates("pos") else {
58        return Err(missing_argument("pos"));
59    };
60    let block_pos = coordinates.block_pos(context.source());
61
62    // Block predicate into block state
63    let Some(block_predicate) = context.block_predicate("block") else {
64        return Err(missing_argument("block"));
65    };
66
67    let (block_state, nbt) = match block_predicate {
68        BlockPredicate::Block {
69            block,
70            properties,
71            nbt,
72        } => {
73            // Adapt properties for the next function
74            let properties_vec: Vec<(&str, &str)> = properties
75                .iter()
76                .map(|(name, value)| (name.as_ref(), value.as_ref()))
77                .collect();
78
79            // Get the block state id
80            let Some(block_state_id) = REGISTRY
81                .blocks
82                .state_id_from_block_properties(block, &properties_vec)
83            else {
84                return Err(CommandSyntaxError::dynamic(
85                    "This Block is not registered or a property name/value is invalid.",
86                ));
87            };
88
89            (block_state_id, nbt)
90        }
91        BlockPredicate::Tag { .. } => unreachable!(),
92    };
93
94    // World the player is in
95    let level = context.source().world();
96
97    // Keep mode throw an error when you try to replace an air block
98    if matches!(mode, SetBlockMode::Keep) && level.get_block_state(block_pos).is_air() {
99        return Ok(set_block_failed(context.source()));
100    }
101
102    let place_needed = if matches!(mode, SetBlockMode::Destroy) {
103        level.destroy_block(block_pos, true);
104
105        !block_state.is_air() || level.get_block_state(block_pos).is_air()
106    } else {
107        true
108    };
109
110    let update_bits = if matches!(mode, SetBlockMode::Strict) {
111        816
112    } else {
113        256
114    };
115
116    // Save the old state to update the neighbors later
117    let old_state = level.get_block_state(block_pos);
118
119    // Place the block and update with the right flag
120    if place_needed
121        && !level.set_block(
122            block_pos,
123            block_state,
124            UpdateFlags::from_bits_truncate(2 | update_bits),
125        )
126    {
127        return Ok(set_block_failed(context.source()));
128    }
129
130    // Load the NBT data into the block entity if it exists
131    if let Some(block_entity) = level.get_block_entity(block_pos)
132        && let Some(nbt) = nbt
133    {
134        let mut bytes = Vec::new();
135        nbt.write(&mut bytes);
136        let borrowed = read_compound(&mut Cursor::new(bytes.as_slice())).map_err(|_| {
137            CommandSyntaxError::dynamic("Failed to transpose owned compound into borrowed ")
138        })?;
139        block_entity.load_additional(&borrowed);
140        block_entity.set_changed();
141    }
142
143    if !matches!(mode, SetBlockMode::Strict) {
144        level.update_neighbour_on_block_set(block_pos, old_state);
145        if block_state.has_fluid() {
146            level.schedule_fluid_tick(
147                block_pos,
148                block_state.get_fluid_state().fluid_id,
149                0,
150                TickPriority::ExtremelyHigh,
151            );
152        }
153    }
154
155    context.source().send_success(
156        &COMMANDS_SETBLOCK_SUCCESS
157            .message([
158                TextComponent::plain(format!("{}", block_pos.x())),
159                TextComponent::plain(format!("{}", block_pos.y())),
160                TextComponent::plain(format!("{}", block_pos.z())),
161            ])
162            .component(),
163        true,
164    );
165
166    Ok(1)
167}
168
169fn missing_argument(name: &str) -> CommandSyntaxError {
170    CommandSyntaxError::dynamic(format!(
171        "Parsed value for {name} is missing from the command context"
172    ))
173}
174
175fn set_block_failed(source: &CommandSource) -> i32 {
176    source.send_failure(COMMANDS_SETBLOCK_FAILED.msg().component());
177
178    // No block placed or replaced
179    0
180}