Skip to main content

steel_core/command/execution/
block.rs

1//! Block-state and block-entity predicates used by commands.
2
3use super::argument::{matches_substring, parse_identifier, unknown_resource};
4use crate::command::brigadier::{
5    CommandSyntaxError, CommandSyntaxErrorKind, StringReader, SuggestionsBuilder,
6};
7use simdnbt::owned::NbtCompound;
8use steel_registry::{
9    BLOCKS_REGISTRY, REGISTRY, RegistryExt as _, TaggedRegistryExt as _, blocks::BlockRef,
10};
11use steel_utils::{BlockStateId, Identifier, nbt::parse_snbt_compound_argument};
12use text_components::TextComponent;
13
14type BlockProperties = Vec<(Box<str>, Box<str>)>;
15
16/// A concrete block or block tag with optional state and block-entity constraints.
17#[derive(Clone, Debug, PartialEq)]
18pub(crate) enum BlockPredicate {
19    Block {
20        block: BlockRef,
21        properties: BlockProperties,
22        nbt: Option<NbtCompound>,
23    },
24    Tag {
25        tag: Identifier,
26        properties: BlockProperties,
27        nbt: Option<NbtCompound>,
28    },
29}
30
31impl BlockPredicate {
32    pub(crate) fn matches_state(&self, state: BlockStateId) -> bool {
33        let Some(actual) = REGISTRY.blocks.by_state_id(state) else {
34            return false;
35        };
36        let properties = match self {
37            Self::Block {
38                block, properties, ..
39            } => {
40                if actual != *block {
41                    return false;
42                }
43                properties
44            }
45            Self::Tag {
46                tag, properties, ..
47            } => {
48                if !actual.has_tag(tag) {
49                    return false;
50                }
51                properties
52            }
53        };
54        state_properties_match(state, properties)
55    }
56
57    pub(crate) const fn nbt(&self) -> Option<&NbtCompound> {
58        match self {
59            Self::Block { nbt, .. } | Self::Tag { nbt, .. } => nbt.as_ref(),
60        }
61    }
62}
63
64fn state_properties_match(state: BlockStateId, expected: &BlockProperties) -> bool {
65    let actual = REGISTRY.blocks.get_properties(state);
66    expected.iter().all(|(name, value)| {
67        actual.iter().any(|(actual_name, actual_value)| {
68            *actual_name == name.as_ref() && *actual_value == value.as_ref()
69        })
70    })
71}
72
73pub(super) fn parse_block_predicate(
74    reader: &mut StringReader<'_>,
75) -> Result<BlockPredicate, CommandSyntaxError> {
76    if reader.peek() == Some('#') {
77        reader.skip();
78        return parse_tag_predicate(reader);
79    }
80    parse_concrete_block_predicate(reader)
81}
82
83fn parse_concrete_block_predicate(
84    reader: &mut StringReader<'_>,
85) -> Result<BlockPredicate, CommandSyntaxError> {
86    let key = parse_identifier(reader)?;
87    let Some(block) = REGISTRY.blocks.by_key(&key) else {
88        return Err(unknown_resource(reader, &key, &BLOCKS_REGISTRY));
89    };
90    let properties = if reader.peek() == Some('[') {
91        parse_properties(reader, Some(block))?
92    } else {
93        Vec::new()
94    };
95    let nbt = parse_optional_nbt(reader)?;
96    Ok(BlockPredicate::Block {
97        block,
98        properties,
99        nbt,
100    })
101}
102
103fn parse_tag_predicate(
104    reader: &mut StringReader<'_>,
105) -> Result<BlockPredicate, CommandSyntaxError> {
106    let key = parse_identifier(reader)?;
107    if !REGISTRY.blocks.tag_keys().any(|tag| tag == &key) {
108        return Err(dynamic_error(reader, format!("Unknown block tag '#{key}'")));
109    }
110    let properties = if reader.peek() == Some('[') {
111        parse_properties(reader, None)?
112    } else {
113        Vec::new()
114    };
115    let nbt = parse_optional_nbt(reader)?;
116    Ok(BlockPredicate::Tag {
117        tag: key,
118        properties,
119        nbt,
120    })
121}
122
123fn parse_properties(
124    reader: &mut StringReader<'_>,
125    block: Option<BlockRef>,
126) -> Result<BlockProperties, CommandSyntaxError> {
127    reader.expect('[')?;
128    reader.skip_whitespace();
129    let mut properties = BlockProperties::new();
130
131    while reader.can_read() && reader.peek() != Some(']') {
132        reader.skip_whitespace();
133        let key = reader.read_string()?;
134        if key.is_empty() {
135            return Err(dynamic_error(reader, "Expected block property name"));
136        }
137        if properties
138            .iter()
139            .any(|(existing, _)| existing.as_ref() == key)
140        {
141            return Err(dynamic_error(
142                reader,
143                format!("Duplicate block property '{key}'"),
144            ));
145        }
146        let property = block.and_then(|block| {
147            block
148                .properties
149                .iter()
150                .copied()
151                .find(|property| property.get_name() == key)
152        });
153        if block.is_some() && property.is_none() {
154            return Err(dynamic_error(
155                reader,
156                format!("Unknown property '{key}' for block predicate"),
157            ));
158        }
159
160        reader.skip_whitespace();
161        reader.expect('=')?;
162        reader.skip_whitespace();
163        let value = reader.read_string()?;
164        if let Some(property) = property
165            && !property
166                .get_possible_value_names()
167                .contains(&value.as_str())
168        {
169            return Err(dynamic_error(
170                reader,
171                format!("Invalid value '{value}' for block property '{key}'"),
172            ));
173        }
174        properties.push((key.into(), value.into()));
175
176        reader.skip_whitespace();
177        match reader.peek() {
178            Some(',') => {
179                reader.skip();
180            }
181            Some(']') => {}
182            _ => return Err(dynamic_error(reader, "Expected ',' or ']'")),
183        }
184    }
185
186    reader.expect(']')?;
187    Ok(properties)
188}
189
190fn parse_optional_nbt(
191    reader: &mut StringReader<'_>,
192) -> Result<Option<NbtCompound>, CommandSyntaxError> {
193    if reader.peek() != Some('{') {
194        return Ok(None);
195    }
196    let parsed = parse_snbt_compound_argument(reader.remaining());
197    let (nbt, consumed) = match parsed {
198        Ok(value) => value,
199        Err(error) => {
200            if !reader.advance_bytes(error.cursor()) {
201                return Err(dynamic_error(reader, "Invalid block entity NBT cursor"));
202            }
203            return Err(dynamic_error(reader, error.component()));
204        }
205    };
206    if !reader.advance_bytes(consumed) {
207        return Err(dynamic_error(reader, "Invalid block entity NBT cursor"));
208    }
209    Ok(Some(nbt))
210}
211
212fn dynamic_error(
213    reader: &StringReader<'_>,
214    message: impl Into<TextComponent>,
215) -> CommandSyntaxError {
216    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message.into())))
217}
218
219pub(super) fn suggest_blocks(builder: &mut SuggestionsBuilder<'_>) {
220    let remaining = builder.remaining_lowercase().to_owned();
221    if remaining.contains(['[', '{']) {
222        return;
223    }
224    if let Some(prefix) = remaining.strip_prefix('#') {
225        for tag in REGISTRY
226            .blocks
227            .tag_keys()
228            .filter(|tag| identifier_matches(prefix, tag))
229        {
230            builder.suggest(format!("#{tag}"));
231        }
232        return;
233    }
234    for block in REGISTRY
235        .blocks
236        .iter()
237        .map(|(_, block)| &block.key)
238        .filter(|key| identifier_matches(&remaining, key))
239    {
240        builder.suggest(block.to_string());
241    }
242    for tag in REGISTRY
243        .blocks
244        .tag_keys()
245        .filter(|tag| identifier_matches(&remaining, tag))
246    {
247        builder.suggest(format!("#{tag}"));
248    }
249}
250
251fn identifier_matches(pattern: &str, identifier: &Identifier) -> bool {
252    if pattern.contains(':') {
253        matches_substring(pattern, &identifier.to_string())
254    } else {
255        matches_substring(pattern, identifier.namespace.as_ref())
256            || matches_substring(pattern, identifier.path.as_ref())
257    }
258}