Skip to main content

steel_core/command/execution/
item.rs

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