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::{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, ¤t[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}