steel_core/command/execution/
block.rs1mod 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#[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 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
99pub(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#[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}