steel_core/command/execution/
block.rs1use 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#[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}