Skip to main content

steel_core/command/execution/
block.rs

1//! Block-state and block-entity predicates used by commands.
2
3mod suggestions;
4
5use std::sync::Arc;
6
7pub(super) use self::suggestions::{suggest_block_inputs, suggest_blocks};
8use super::argument::parse_identifier;
9use crate::{
10    command::brigadier::{CommandSyntaxError, CommandSyntaxErrorKind, StringReader},
11    world::World,
12};
13use simdnbt::owned::NbtCompound;
14use steel_registry::{
15    REGISTRY, RegistryExt as _, TaggedRegistryExt as _,
16    blocks::{BlockRef, block_state_ext::BlockStateExt},
17};
18use steel_utils::{
19    BlockPos, BlockStateId, Identifier,
20    nbt::{compare_nbt_compounds, nbt_compounds_equal, parse_snbt_compound_argument},
21    translations,
22    types::UpdateFlags,
23};
24use text_components::TextComponent;
25
26type BlockProperties = Vec<(Box<str>, Box<str>)>;
27
28/// A parsed concrete block state and optional block-entity data.
29#[derive(Clone, Debug, PartialEq)]
30pub(crate) struct BlockInput {
31    state: BlockStateId,
32    properties: BlockProperties,
33    nbt: Option<NbtCompound>,
34}
35
36impl BlockInput {
37    pub(crate) const fn from_state(state: BlockStateId) -> Self {
38        Self {
39            state,
40            properties: Vec::new(),
41            nbt: None,
42        }
43    }
44
45    pub(crate) const fn state(&self) -> BlockStateId {
46        self.state
47    }
48
49    #[cfg(test)]
50    pub(crate) const fn nbt(&self) -> Option<&NbtCompound> {
51        self.nbt.as_ref()
52    }
53
54    /// Places this input with Vanilla's command block-state semantics.
55    pub(crate) fn place(
56        &self,
57        world: &Arc<World>,
58        pos: BlockPos,
59        flags: UpdateFlags,
60    ) -> Result<bool, CommandSyntaxError> {
61        let mut state = if flags.contains(UpdateFlags::UPDATE_KNOWN_SHAPE) {
62            self.state
63        } else {
64            world.update_from_neighbor_shapes(self.state, pos)
65        };
66        if state.is_air() {
67            state = self.state;
68        }
69        for (name, value) in &self.properties {
70            state = REGISTRY
71                .blocks
72                .try_set_property_by_name(state, name, value)
73                .unwrap_or(state);
74        }
75
76        let mut affected = world.set_block(pos, state, flags);
77        if let Some(nbt) = &self.nbt
78            && let Some(block_entity) = world.get_block_entity(pos)
79        {
80            let before = block_entity.save_without_metadata();
81            block_entity
82                .load_with_owned_components(nbt)
83                .map_err(|error| {
84                    CommandSyntaxError::dynamic(format!(
85                        "Failed to apply block entity NBT at {pos:?}: {error}"
86                    ))
87                })?;
88            let after = block_entity.save_without_metadata();
89            if !nbt_compounds_equal(&before, &after) {
90                affected = true;
91                block_entity.set_changed();
92                world.send_block_updated(pos);
93            }
94        }
95        Ok(affected)
96    }
97}
98
99/// Block-update flags for command placement, vanilla's `2 | (strict ? 816 : 256)`.
100pub(crate) fn placement_flags(strict: bool) -> UpdateFlags {
101    let mut flags = UpdateFlags::UPDATE_CLIENTS | UpdateFlags::UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS;
102    if strict {
103        flags |= UpdateFlags::UPDATE_KNOWN_SHAPE
104            | UpdateFlags::UPDATE_SUPPRESS_DROPS
105            | UpdateFlags::UPDATE_SKIP_ON_PLACE;
106    }
107    flags
108}
109
110/// A concrete block or block tag with optional state and block-entity constraints.
111#[derive(Clone, Debug, PartialEq)]
112pub(crate) enum BlockPredicate {
113    Block {
114        block: BlockRef,
115        properties: BlockProperties,
116        nbt: Option<NbtCompound>,
117    },
118    Tag {
119        tag: Identifier,
120        properties: BlockProperties,
121        nbt: Option<NbtCompound>,
122    },
123}
124
125impl BlockPredicate {
126    pub(crate) fn matches(&self, world: &World, pos: BlockPos) -> bool {
127        if !self.matches_state(world.get_block_state(pos)) {
128            return false;
129        }
130        let Some(expected_nbt) = self.nbt() else {
131            return true;
132        };
133        let Some(block_entity) = world.get_block_entity(pos) else {
134            return false;
135        };
136        compare_nbt_compounds(expected_nbt, &block_entity.save_with_full_metadata(), true)
137    }
138    pub(crate) fn matches_state(&self, state: BlockStateId) -> bool {
139        let Some(actual) = REGISTRY.blocks.by_state_id(state) else {
140            return false;
141        };
142        let properties = match self {
143            Self::Block {
144                block, properties, ..
145            } => {
146                if actual != *block {
147                    return false;
148                }
149                properties
150            }
151            Self::Tag {
152                tag, properties, ..
153            } => {
154                if !actual.has_tag(tag) {
155                    return false;
156                }
157                properties
158            }
159        };
160        state_properties_match(state, properties)
161    }
162
163    pub(crate) const fn nbt(&self) -> Option<&NbtCompound> {
164        match self {
165            Self::Block { nbt, .. } | Self::Tag { nbt, .. } => nbt.as_ref(),
166        }
167    }
168}
169
170fn state_properties_match(state: BlockStateId, expected: &BlockProperties) -> bool {
171    let actual = REGISTRY.blocks.get_properties(state);
172    expected.iter().all(|(name, value)| {
173        actual.iter().any(|(actual_name, actual_value)| {
174            *actual_name == name.as_ref() && *actual_value == value.as_ref()
175        })
176    })
177}
178
179pub(super) fn parse_block_predicate(
180    reader: &mut StringReader<'_>,
181) -> Result<BlockPredicate, CommandSyntaxError> {
182    if reader.peek() == Some('#') {
183        return parse_tag_predicate(reader);
184    }
185    parse_concrete_block_predicate(reader)
186}
187
188fn parse_concrete_block_predicate(
189    reader: &mut StringReader<'_>,
190) -> Result<BlockPredicate, CommandSyntaxError> {
191    let parsed = parse_concrete_block(reader)?;
192    Ok(BlockPredicate::Block {
193        block: parsed.block,
194        properties: parsed.properties,
195        nbt: parsed.nbt,
196    })
197}
198
199pub(super) fn parse_block_input(
200    reader: &mut StringReader<'_>,
201) -> Result<BlockInput, CommandSyntaxError> {
202    if reader.peek() == Some('#') {
203        return Err(dynamic_error(
204            reader,
205            TextComponent::from(&translations::ARGUMENT_BLOCK_TAG_DISALLOWED),
206        ));
207    }
208    let parsed = parse_concrete_block(reader)?;
209    let properties = parsed
210        .properties
211        .iter()
212        .map(|(name, value)| (name.as_ref(), value.as_ref()));
213    let Some(state) = REGISTRY
214        .blocks
215        .state_id_from_block_defaulted_properties(parsed.block, properties)
216    else {
217        return Err(dynamic_error(
218            reader,
219            "Parsed block properties did not resolve to a registered state",
220        ));
221    };
222    Ok(BlockInput {
223        state,
224        properties: parsed.properties,
225        nbt: parsed.nbt,
226    })
227}
228
229struct ParsedConcreteBlock {
230    block: BlockRef,
231    properties: BlockProperties,
232    nbt: Option<NbtCompound>,
233}
234
235fn parse_concrete_block(
236    reader: &mut StringReader<'_>,
237) -> Result<ParsedConcreteBlock, CommandSyntaxError> {
238    let start = reader.checkpoint();
239    let key = parse_identifier(reader)?;
240    let Some(block) = REGISTRY.blocks.by_key(&key) else {
241        reader.restore(start);
242        return Err(unknown_block(reader, &key));
243    };
244    let properties = if reader.peek() == Some('[') {
245        parse_properties(reader, Some(block), &key.to_string())?
246    } else {
247        Vec::new()
248    };
249    let nbt = parse_optional_nbt(reader)?;
250    Ok(ParsedConcreteBlock {
251        block,
252        properties,
253        nbt,
254    })
255}
256
257fn parse_tag_predicate(
258    reader: &mut StringReader<'_>,
259) -> Result<BlockPredicate, CommandSyntaxError> {
260    let start = reader.checkpoint();
261    reader.skip();
262    let key = parse_identifier(reader)?;
263    if !REGISTRY.blocks.tag_keys().any(|tag| tag == &key) {
264        reader.restore(start);
265        return Err(unknown_block_tag(reader, &key));
266    }
267    let properties = if reader.peek() == Some('[') {
268        parse_properties(reader, None, "minecraft:")?
269    } else {
270        Vec::new()
271    };
272    let nbt = parse_optional_nbt(reader)?;
273    Ok(BlockPredicate::Tag {
274        tag: key,
275        properties,
276        nbt,
277    })
278}
279
280fn parse_properties(
281    reader: &mut StringReader<'_>,
282    block: Option<BlockRef>,
283    block_name: &str,
284) -> Result<BlockProperties, CommandSyntaxError> {
285    reader.expect('[')?;
286    reader.skip_whitespace();
287    let mut properties = BlockProperties::new();
288    let mut vague_value_start = None;
289
290    while reader.can_read() && reader.peek() != Some(']') {
291        reader.skip_whitespace();
292        let key_start = reader.checkpoint();
293        let key = reader.read_string()?;
294        if properties
295            .iter()
296            .any(|(existing, _)| existing.as_ref() == key)
297        {
298            reader.restore(key_start);
299            return Err(duplicate_property(reader, block_name, &key));
300        }
301        let property = block.and_then(|block| {
302            block
303                .properties
304                .iter()
305                .copied()
306                .find(|property| property.get_name() == key)
307        });
308        if block.is_some() && property.is_none() {
309            reader.restore(key_start);
310            return Err(unknown_property(reader, block_name, &key));
311        }
312
313        reader.skip_whitespace();
314        if reader.peek() != Some('=') {
315            if block.is_none() {
316                reader.restore(key_start);
317            }
318            return Err(expected_property_value(reader, block_name, &key));
319        }
320        reader.skip();
321        reader.skip_whitespace();
322        let value_start = reader.checkpoint();
323        let value = reader.read_string()?;
324        if let Some(property) = property
325            && !property
326                .get_possible_value_names()
327                .contains(&value.as_str())
328        {
329            reader.restore(value_start);
330            return Err(invalid_property_value(reader, block_name, &key, &value));
331        }
332        vague_value_start = block.is_none().then_some(value_start);
333        properties.push((key.into(), value.into()));
334
335        reader.skip_whitespace();
336        match reader.peek() {
337            Some(',') => {
338                reader.skip();
339                vague_value_start = None;
340            }
341            Some(']') => {}
342            Some(_) => return Err(unclosed_properties(reader)),
343            None => break,
344        }
345    }
346
347    if reader.peek() != Some(']') {
348        if let Some(value_start) = vague_value_start {
349            reader.restore(value_start);
350        }
351        return Err(unclosed_properties(reader));
352    }
353    reader.skip();
354    Ok(properties)
355}
356
357fn unknown_block(reader: &StringReader<'_>, key: &Identifier) -> CommandSyntaxError {
358    let message = translations::ARGUMENT_BLOCK_ID_INVALID
359        .message([key.to_string()])
360        .component();
361    dynamic_error(reader, message)
362}
363
364fn unknown_block_tag(reader: &StringReader<'_>, key: &Identifier) -> CommandSyntaxError {
365    let message = translations::ARGUMENTS_BLOCK_TAG_UNKNOWN
366        .message([key.to_string()])
367        .component();
368    dynamic_error(reader, message)
369}
370
371fn unknown_property(reader: &StringReader<'_>, block: &str, property: &str) -> CommandSyntaxError {
372    let message = translations::ARGUMENT_BLOCK_PROPERTY_UNKNOWN
373        .message([block.to_owned(), property.to_owned()])
374        .component();
375    dynamic_error(reader, message)
376}
377
378fn duplicate_property(
379    reader: &StringReader<'_>,
380    block: &str,
381    property: &str,
382) -> CommandSyntaxError {
383    let message = translations::ARGUMENT_BLOCK_PROPERTY_DUPLICATE
384        .message([property.to_owned(), block.to_owned()])
385        .component();
386    dynamic_error(reader, message)
387}
388
389fn invalid_property_value(
390    reader: &StringReader<'_>,
391    block: &str,
392    property: &str,
393    value: &str,
394) -> CommandSyntaxError {
395    let message = translations::ARGUMENT_BLOCK_PROPERTY_INVALID
396        .message([block.to_owned(), value.to_owned(), property.to_owned()])
397        .component();
398    dynamic_error(reader, message)
399}
400
401fn expected_property_value(
402    reader: &StringReader<'_>,
403    block: &str,
404    property: &str,
405) -> CommandSyntaxError {
406    let message = translations::ARGUMENT_BLOCK_PROPERTY_NOVALUE
407        .message([property.to_owned(), block.to_owned()])
408        .component();
409    dynamic_error(reader, message)
410}
411
412fn unclosed_properties(reader: &StringReader<'_>) -> CommandSyntaxError {
413    dynamic_error(
414        reader,
415        TextComponent::from(&translations::ARGUMENT_BLOCK_PROPERTY_UNCLOSED),
416    )
417}
418
419fn parse_optional_nbt(
420    reader: &mut StringReader<'_>,
421) -> Result<Option<NbtCompound>, CommandSyntaxError> {
422    if reader.peek() != Some('{') {
423        return Ok(None);
424    }
425    let parsed = parse_snbt_compound_argument(reader.remaining());
426    let (nbt, consumed) = match parsed {
427        Ok(value) => value,
428        Err(error) => {
429            if !reader.advance_bytes(error.cursor()) {
430                return Err(dynamic_error(reader, "Invalid block entity NBT cursor"));
431            }
432            return Err(dynamic_error(reader, error.component()));
433        }
434    };
435    if !reader.advance_bytes(consumed) {
436        return Err(dynamic_error(reader, "Invalid block entity NBT cursor"));
437    }
438    Ok(Some(nbt))
439}
440
441fn dynamic_error(
442    reader: &StringReader<'_>,
443    message: impl Into<TextComponent>,
444) -> CommandSyntaxError {
445    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message.into())))
446}