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    expected.iter().all(|(name, value)| {
172        REGISTRY.blocks.get_property_str(state, name.as_ref()) == Some(value.as_ref())
173    })
174}
175
176pub(super) fn parse_block_predicate(
177    reader: &mut StringReader<'_>,
178) -> Result<BlockPredicate, CommandSyntaxError> {
179    if reader.peek() == Some('#') {
180        return parse_tag_predicate(reader);
181    }
182    parse_concrete_block_predicate(reader)
183}
184
185fn parse_concrete_block_predicate(
186    reader: &mut StringReader<'_>,
187) -> Result<BlockPredicate, CommandSyntaxError> {
188    let parsed = parse_concrete_block(reader)?;
189    Ok(BlockPredicate::Block {
190        block: parsed.block,
191        properties: parsed.properties,
192        nbt: parsed.nbt,
193    })
194}
195
196pub(super) fn parse_block_input(
197    reader: &mut StringReader<'_>,
198) -> Result<BlockInput, CommandSyntaxError> {
199    if reader.peek() == Some('#') {
200        return Err(dynamic_error(
201            reader,
202            TextComponent::from(&translations::ARGUMENT_BLOCK_TAG_DISALLOWED),
203        ));
204    }
205    let parsed = parse_concrete_block(reader)?;
206    let properties = parsed
207        .properties
208        .iter()
209        .map(|(name, value)| (name.as_ref(), value.as_ref()));
210    let Some(state) = REGISTRY
211        .blocks
212        .state_id_from_block_defaulted_properties(parsed.block, properties)
213    else {
214        return Err(dynamic_error(
215            reader,
216            "Parsed block properties did not resolve to a registered state",
217        ));
218    };
219    Ok(BlockInput {
220        state,
221        properties: parsed.properties,
222        nbt: parsed.nbt,
223    })
224}
225
226struct ParsedConcreteBlock {
227    block: BlockRef,
228    properties: BlockProperties,
229    nbt: Option<NbtCompound>,
230}
231
232fn parse_concrete_block(
233    reader: &mut StringReader<'_>,
234) -> Result<ParsedConcreteBlock, CommandSyntaxError> {
235    let start = reader.checkpoint();
236    let key = parse_identifier(reader)?;
237    let Some(block) = REGISTRY.blocks.by_key(&key) else {
238        reader.restore(start);
239        return Err(unknown_block(reader, &key));
240    };
241    let properties = if reader.peek() == Some('[') {
242        parse_properties(reader, Some(block), &key.to_string())?
243    } else {
244        Vec::new()
245    };
246    let nbt = parse_optional_nbt(reader)?;
247    Ok(ParsedConcreteBlock {
248        block,
249        properties,
250        nbt,
251    })
252}
253
254fn parse_tag_predicate(
255    reader: &mut StringReader<'_>,
256) -> Result<BlockPredicate, CommandSyntaxError> {
257    let start = reader.checkpoint();
258    reader.skip();
259    let key = parse_identifier(reader)?;
260    if !REGISTRY.blocks.tag_keys().any(|tag| tag == &key) {
261        reader.restore(start);
262        return Err(unknown_block_tag(reader, &key));
263    }
264    let properties = if reader.peek() == Some('[') {
265        parse_properties(reader, None, "minecraft:")?
266    } else {
267        Vec::new()
268    };
269    let nbt = parse_optional_nbt(reader)?;
270    Ok(BlockPredicate::Tag {
271        tag: key,
272        properties,
273        nbt,
274    })
275}
276
277fn parse_properties(
278    reader: &mut StringReader<'_>,
279    block: Option<BlockRef>,
280    block_name: &str,
281) -> Result<BlockProperties, CommandSyntaxError> {
282    reader.expect('[')?;
283    reader.skip_whitespace();
284    let mut properties = BlockProperties::new();
285    let mut vague_value_start = None;
286
287    while reader.can_read() && reader.peek() != Some(']') {
288        reader.skip_whitespace();
289        let key_start = reader.checkpoint();
290        let key = reader.read_string()?;
291        if properties
292            .iter()
293            .any(|(existing, _)| existing.as_ref() == key)
294        {
295            reader.restore(key_start);
296            return Err(duplicate_property(reader, block_name, &key));
297        }
298        let property = block.and_then(|block| {
299            block
300                .properties
301                .iter()
302                .copied()
303                .find(|property| property.get_name() == key)
304        });
305        if block.is_some() && property.is_none() {
306            reader.restore(key_start);
307            return Err(unknown_property(reader, block_name, &key));
308        }
309
310        reader.skip_whitespace();
311        if reader.peek() != Some('=') {
312            if block.is_none() {
313                reader.restore(key_start);
314            }
315            return Err(expected_property_value(reader, block_name, &key));
316        }
317        reader.skip();
318        reader.skip_whitespace();
319        let value_start = reader.checkpoint();
320        let value = reader.read_string()?;
321        if let Some(property) = property
322            && !property
323                .get_possible_value_names()
324                .contains(&value.as_str())
325        {
326            reader.restore(value_start);
327            return Err(invalid_property_value(reader, block_name, &key, &value));
328        }
329        vague_value_start = block.is_none().then_some(value_start);
330        properties.push((key.into(), value.into()));
331
332        reader.skip_whitespace();
333        match reader.peek() {
334            Some(',') => {
335                reader.skip();
336                vague_value_start = None;
337            }
338            Some(']') => {}
339            Some(_) => return Err(unclosed_properties(reader)),
340            None => break,
341        }
342    }
343
344    if reader.peek() != Some(']') {
345        if let Some(value_start) = vague_value_start {
346            reader.restore(value_start);
347        }
348        return Err(unclosed_properties(reader));
349    }
350    reader.skip();
351    Ok(properties)
352}
353
354fn unknown_block(reader: &StringReader<'_>, key: &Identifier) -> CommandSyntaxError {
355    let message = translations::ARGUMENT_BLOCK_ID_INVALID
356        .message([key.to_string()])
357        .component();
358    dynamic_error(reader, message)
359}
360
361fn unknown_block_tag(reader: &StringReader<'_>, key: &Identifier) -> CommandSyntaxError {
362    let message = translations::ARGUMENTS_BLOCK_TAG_UNKNOWN
363        .message([key.to_string()])
364        .component();
365    dynamic_error(reader, message)
366}
367
368fn unknown_property(reader: &StringReader<'_>, block: &str, property: &str) -> CommandSyntaxError {
369    let message = translations::ARGUMENT_BLOCK_PROPERTY_UNKNOWN
370        .message([block.to_owned(), property.to_owned()])
371        .component();
372    dynamic_error(reader, message)
373}
374
375fn duplicate_property(
376    reader: &StringReader<'_>,
377    block: &str,
378    property: &str,
379) -> CommandSyntaxError {
380    let message = translations::ARGUMENT_BLOCK_PROPERTY_DUPLICATE
381        .message([property.to_owned(), block.to_owned()])
382        .component();
383    dynamic_error(reader, message)
384}
385
386fn invalid_property_value(
387    reader: &StringReader<'_>,
388    block: &str,
389    property: &str,
390    value: &str,
391) -> CommandSyntaxError {
392    let message = translations::ARGUMENT_BLOCK_PROPERTY_INVALID
393        .message([block.to_owned(), value.to_owned(), property.to_owned()])
394        .component();
395    dynamic_error(reader, message)
396}
397
398fn expected_property_value(
399    reader: &StringReader<'_>,
400    block: &str,
401    property: &str,
402) -> CommandSyntaxError {
403    let message = translations::ARGUMENT_BLOCK_PROPERTY_NOVALUE
404        .message([property.to_owned(), block.to_owned()])
405        .component();
406    dynamic_error(reader, message)
407}
408
409fn unclosed_properties(reader: &StringReader<'_>) -> CommandSyntaxError {
410    dynamic_error(
411        reader,
412        TextComponent::from(&translations::ARGUMENT_BLOCK_PROPERTY_UNCLOSED),
413    )
414}
415
416fn parse_optional_nbt(
417    reader: &mut StringReader<'_>,
418) -> Result<Option<NbtCompound>, CommandSyntaxError> {
419    if reader.peek() != Some('{') {
420        return Ok(None);
421    }
422    let parsed = parse_snbt_compound_argument(reader.remaining());
423    let (nbt, consumed) = match parsed {
424        Ok(value) => value,
425        Err(error) => {
426            if !reader.advance_bytes(error.cursor()) {
427                return Err(dynamic_error(reader, "Invalid block entity NBT cursor"));
428            }
429            return Err(dynamic_error(reader, error.component()));
430        }
431    };
432    if !reader.advance_bytes(consumed) {
433        return Err(dynamic_error(reader, "Invalid block entity NBT cursor"));
434    }
435    Ok(Some(nbt))
436}
437
438fn dynamic_error(
439    reader: &StringReader<'_>,
440    message: impl Into<TextComponent>,
441) -> CommandSyntaxError {
442    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message.into())))
443}