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::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, &current[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}