steel_core/command/execution/
item.rs1use rustc_hash::FxHashSet;
4use simdnbt::owned::NbtTag;
5use steel_registry::{
6 REGISTRY, RegistryExt as _,
7 data_components::{ComponentData, ComponentEntry, DataComponentPatch, vanilla_components},
8 item_stack::ItemStack,
9 items::ItemRef,
10};
11use steel_utils::{
12 Identifier,
13 nbt::{NbtNumeric as _, parse_snbt_argument},
14 translations,
15};
16
17use crate::command::brigadier::{
18 CommandSyntaxError, CommandSyntaxErrorKind, StringReader, SuggestionsBuilder,
19};
20
21use super::argument::parse_identifier;
22use super::suggestions::matches_substring;
23
24pub(super) fn parse_item_stack(
25 reader: &mut StringReader<'_>,
26) -> Result<ItemStack, CommandSyntaxError> {
27 let start = reader.checkpoint();
28 let result = parse_item_stack_inner(reader);
29 if result.is_err() {
30 reader.restore(start);
31 }
32 result
33}
34
35fn parse_item_stack_inner(reader: &mut StringReader<'_>) -> Result<ItemStack, CommandSyntaxError> {
36 let item_start = reader.checkpoint();
37 let item_key = parse_identifier(reader)?;
38 let Some(item) = REGISTRY.items.by_key(&item_key) else {
39 reader.restore(item_start);
40 let message = translations::ARGUMENT_ITEM_ID_INVALID
41 .message([item_key.to_string()])
42 .component();
43 return Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message))));
44 };
45
46 let mut patch = DataComponentPatch::new();
47 if reader.peek() == Some('[') {
48 parse_components(reader, &mut patch)?;
49 }
50
51 let stack = ItemStack::with_count_and_patch(item, 1, patch);
52 validate_item_stack(reader, &stack)?;
53 Ok(stack)
54}
55
56fn parse_components(
57 reader: &mut StringReader<'_>,
58 patch: &mut DataComponentPatch,
59) -> Result<(), CommandSyntaxError> {
60 reader.expect('[')?;
61 let mut visited = FxHashSet::default();
62
63 loop {
64 reader.skip_whitespace();
65 if reader.peek() == Some(']') {
66 reader.skip();
67 return Ok(());
68 }
69 if !reader.can_read() {
70 return Err(expected_component(reader));
71 }
72
73 let removed = if reader.peek() == Some('!') {
74 reader.skip();
75 true
76 } else {
77 false
78 };
79 let key = parse_component_key(reader)?;
80 if !visited.insert(key.clone()) {
81 let message = translations::ARGUMENTS_ITEM_COMPONENT_REPEATED
82 .message([key.to_string()])
83 .component();
84 return Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message))));
85 }
86
87 if removed {
88 patch.remove_raw(key);
89 } else {
90 parse_component_value(reader, patch, key)?;
91 }
92
93 reader.skip_whitespace();
94 if reader.peek() != Some(',') {
95 reader.expect(']')?;
96 return Ok(());
97 }
98 reader.skip();
99 reader.skip_whitespace();
100 if !reader.can_read() {
101 return Err(expected_component(reader));
102 }
103 }
104}
105
106fn parse_component_key(reader: &mut StringReader<'_>) -> Result<Identifier, CommandSyntaxError> {
107 let start = reader.checkpoint();
108 let key = parse_identifier(reader)?;
109 let Some(entry) = REGISTRY.data_components.by_key(&key) else {
110 reader.restore(start);
111 return Err(unknown_component(reader, &key));
112 };
113 if !entry.is_persistent() {
114 reader.restore(start);
115 return Err(unknown_component(reader, &key));
116 }
117 Ok(key)
118}
119
120fn parse_component_value(
121 reader: &mut StringReader<'_>,
122 patch: &mut DataComponentPatch,
123 key: Identifier,
124) -> Result<(), CommandSyntaxError> {
125 reader.skip_whitespace();
126 reader.expect('=')?;
127 reader.skip_whitespace();
128
129 let Some(entry) = REGISTRY.data_components.by_key(&key) else {
130 return Err(unknown_component(reader, &key));
131 };
132 let (tag, consumed) = parse_snbt_argument(reader.remaining()).map_err(|error| {
133 reader.advance_bytes(error.cursor());
134 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(error.component())))
135 })?;
136 if !reader.advance_bytes(consumed) {
137 return Err(malformed_component(
138 reader,
139 &key,
140 "component value ended at an invalid UTF-8 boundary",
141 ));
142 }
143 let Some(value) = read_component_value(entry, &tag) else {
144 return Err(malformed_component(
145 reader,
146 &key,
147 "value does not match the component codec",
148 ));
149 };
150 if !component_value_is_valid(&key, &value) {
151 return Err(malformed_component(
152 reader,
153 &key,
154 "value is outside the vanilla component range",
155 ));
156 }
157 if !patch.set_raw(key.clone(), value) {
158 return Err(malformed_component(
159 reader,
160 &key,
161 "value does not match the registered component type",
162 ));
163 }
164 Ok(())
165}
166
167pub(super) fn read_component_value(entry: &ComponentEntry, tag: &NbtTag) -> Option<ComponentData> {
168 entry.read_nbt_owned(tag)
169}
170
171pub(super) fn numeric_i32(tag: &NbtTag) -> Option<i32> {
172 tag.codec_i32()
173}
174
175pub(super) fn component_value_is_valid(key: &Identifier, value: &ComponentData) -> bool {
176 if key == vanilla_components::MAX_STACK_SIZE.key() {
177 return value
178 .downcast_ref::<i32>()
179 .is_some_and(|value| (1..=99).contains(value));
180 }
181 if key == vanilla_components::MAX_DAMAGE.key() {
182 return value.downcast_ref::<i32>().is_some_and(|value| *value > 0);
183 }
184 if key == vanilla_components::DAMAGE.key() || key == vanilla_components::REPAIR_COST.key() {
185 return value.downcast_ref::<i32>().is_some_and(|value| *value >= 0);
186 }
187 if key == vanilla_components::MINIMUM_ATTACK_CHARGE.key() {
188 return value
189 .downcast_ref::<f32>()
190 .is_some_and(|value| value.is_finite() && !value.is_sign_negative() && *value <= 1.0);
191 }
192 if key == vanilla_components::POTION_DURATION_SCALE.key() {
193 return value
194 .downcast_ref::<f32>()
195 .is_some_and(|value| value.is_finite() && !value.is_sign_negative());
196 }
197
198 true
199}
200
201fn validate_item_stack(
202 reader: &StringReader<'_>,
203 stack: &ItemStack,
204) -> Result<(), CommandSyntaxError> {
205 stack
206 .validate_strict()
207 .map_err(|error| malformed_item(reader, &error.to_string()))
208}
209
210fn expected_component(reader: &StringReader<'_>) -> CommandSyntaxError {
211 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
212 (&translations::ARGUMENTS_ITEM_COMPONENT_EXPECTED).into(),
213 )))
214}
215
216fn unknown_component(reader: &StringReader<'_>, key: &Identifier) -> CommandSyntaxError {
217 let message = translations::ARGUMENTS_ITEM_COMPONENT_UNKNOWN
218 .message([key.to_string()])
219 .component();
220 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message)))
221}
222
223fn malformed_component(
224 reader: &StringReader<'_>,
225 key: &Identifier,
226 error: &str,
227) -> CommandSyntaxError {
228 let message = translations::ARGUMENTS_ITEM_COMPONENT_MALFORMED
229 .message([key.to_string(), error.to_owned()])
230 .component();
231 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message)))
232}
233
234fn malformed_item(reader: &StringReader<'_>, error: &str) -> CommandSyntaxError {
235 let message = translations::ARGUMENTS_ITEM_MALFORMED
236 .message([error.to_owned()])
237 .component();
238 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message)))
239}
240
241pub(super) fn suggest_item_stack(builder: &mut SuggestionsBuilder<'_>) {
242 let input = builder.remaining();
243 let Some(component_start) = input.find('[') else {
244 suggest_items(input, builder);
245 if item_by_input(input).is_some() {
246 builder.suggest(format!("{input}["));
247 }
248 return;
249 };
250 if item_by_input(&input[..component_start]).is_none() {
251 return;
252 }
253
254 let components = &input[component_start + 1..];
255 let Some(current_start) = current_component_start(components) else {
256 return;
257 };
258 let current = &components[current_start..];
259 let trimmed = current.trim_start();
260 let whitespace = current.len() - trimmed.len();
261 let current = trimmed;
262 let prefix = &input[..component_start + 1 + current_start + whitespace];
263
264 if let Some(value_start) = current.find('=') {
265 suggest_component_delimiters(input, ¤t[value_start + 1..], builder);
266 return;
267 }
268
269 let (removed, component_prefix) = current
270 .strip_prefix('!')
271 .map_or((false, current), |prefix| (true, prefix));
272 let component_key = component_prefix.trim_end();
273 if component_key.len() != component_prefix.len() && component_key.is_empty() {
274 return;
275 }
276 if component_by_input(component_key).is_some() {
277 if removed {
278 suggest_operation_delimiters(input, builder);
279 return;
280 }
281 builder.suggest(format!("{input}="));
282 return;
283 }
284 let visited = visited_component_keys(&components[..current_start]);
285 for entry in
286 (0..REGISTRY.data_components.len()).filter_map(|id| REGISTRY.data_components.by_id(id))
287 {
288 if !entry.is_persistent()
289 || visited.contains(&entry.key)
290 || !resource_matches(component_prefix, &entry.key)
291 {
292 continue;
293 }
294 let operation = if removed { "!" } else { "" };
295 let suffix = if removed { "" } else { "=" };
296 builder.suggest(format!("{prefix}{operation}{}{suffix}", entry.key));
297 }
298 if current.is_empty() {
299 builder.suggest(format!("{prefix}!"));
300 }
301}
302
303fn suggest_items(input: &str, builder: &mut SuggestionsBuilder<'_>) {
304 for (_, item) in REGISTRY.items.iter() {
305 if resource_matches(input, &item.key) {
306 builder.suggest(item.key.to_string());
307 }
308 }
309}
310
311fn item_by_input(input: &str) -> Option<ItemRef> {
312 let key = parse_identifier_text(input)?;
313 REGISTRY.items.by_key(&key)
314}
315
316fn component_by_input(input: &str) -> Option<&'static ComponentEntry> {
317 let key = parse_identifier_text(input)?;
318 let entry = REGISTRY.data_components.by_key(&key)?;
319 entry.is_persistent().then_some(entry)
320}
321
322fn parse_identifier_text(input: &str) -> Option<Identifier> {
323 let mut reader = StringReader::new(input);
324 let key = parse_identifier(&mut reader).ok()?;
325 (!reader.can_read()).then_some(key)
326}
327
328fn resource_matches(pattern: &str, key: &Identifier) -> bool {
329 let pattern = pattern.to_lowercase();
330 if pattern.contains(':') {
331 return matches_substring(&pattern, &key.to_string());
332 }
333 matches_substring(&pattern, key.namespace.as_ref())
334 || matches_substring(&pattern, key.path.as_ref())
335}
336
337fn current_component_start(components: &str) -> Option<usize> {
338 let mut depth = 0usize;
339 let mut quote = None;
340 let mut escaped = false;
341 let mut current_start = 0usize;
342
343 for (index, character) in components.char_indices() {
344 if let Some(terminator) = quote {
345 if escaped {
346 escaped = false;
347 } else if character == '\\' {
348 escaped = true;
349 } else if character == terminator {
350 quote = None;
351 }
352 continue;
353 }
354 match character {
355 '"' | '\'' => quote = Some(character),
356 '{' | '[' => depth += 1,
357 '}' | ']' if depth > 0 => depth -= 1,
358 ']' => return None,
359 ',' if depth == 0 => current_start = index + character.len_utf8(),
360 _ => {}
361 }
362 }
363 Some(current_start)
364}
365
366fn visited_component_keys(components: &str) -> FxHashSet<Identifier> {
367 components
368 .split(',')
369 .filter_map(|component| {
370 let component = component.trim().trim_start_matches('!');
371 let key = component.split_once('=').map_or(component, |(key, _)| key);
372 parse_identifier_text(key.trim())
373 })
374 .collect()
375}
376
377fn suggest_component_delimiters(input: &str, value: &str, builder: &mut SuggestionsBuilder<'_>) {
378 let Ok((_, consumed)) = parse_snbt_argument(value) else {
379 return;
380 };
381 if value[consumed..].trim().is_empty() {
382 suggest_operation_delimiters(input, builder);
383 }
384}
385
386fn suggest_operation_delimiters(input: &str, builder: &mut SuggestionsBuilder<'_>) {
387 builder.suggest(format!("{input},"));
388 builder.suggest(format!("{input}]"));
389}