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 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}