1use std::cell::Cell;
4use std::io::{Cursor, Error, Result, Write};
5use std::str::FromStr;
6
7use simdnbt::owned::{NbtCompound, NbtTag};
8use simdnbt::{FromNbtTag, ToNbtTag};
9use steel_utils::codec::VarInt;
10use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
11use steel_utils::nbt::NbtNumeric as _;
12use steel_utils::serial::{ReadFrom, WriteTo};
13use steel_utils::{DowncastType, Identifier};
14use text_components::{EncodedNbt, interactivity::HoverEvent};
15
16use crate::data_components::vanilla_components::MAX_STACK_SIZE;
17use crate::data_components::{
18 Component, ComponentData, ComponentPatchEntry, DataComponentPatch, DataComponentType,
19};
20use crate::item_stack::ItemStack;
21use crate::items::ItemRef;
22use crate::{REGISTRY, RegistryEntry, RegistryExt, vanilla_items};
23
24const MAX_TEMPLATE_CODEC_DEPTH: usize = 512;
27
28thread_local! {
29 static TEMPLATE_CODEC_DEPTH: Cell<usize> = const { Cell::new(0) };
30}
31
32struct TemplateDepthGuard;
33
34impl TemplateDepthGuard {
35 fn enter() -> Result<Self> {
36 TEMPLATE_CODEC_DEPTH.with(|depth| {
37 let current = depth.get();
38 if current >= MAX_TEMPLATE_CODEC_DEPTH {
39 return Err(Error::other(format!(
40 "Item stack template nesting exceeds {MAX_TEMPLATE_CODEC_DEPTH}"
41 )));
42 }
43 depth.set(current + 1);
44 Ok(Self)
45 })
46 }
47}
48
49impl Drop for TemplateDepthGuard {
50 fn drop(&mut self) {
51 TEMPLATE_CODEC_DEPTH.with(|depth| depth.set(depth.get() - 1));
52 }
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub struct ItemStackTemplate {
58 item: ItemRef,
59 count: i32,
60 components: DataComponentPatch,
61 components_hash: Option<i32>,
62}
63
64impl ItemStackTemplate {
65 pub const MIN_COUNT: i32 = 1;
66 pub const MAX_COUNT: i32 = 99;
67
68 #[must_use]
70 pub fn new(item: ItemRef) -> Self {
71 assert!(
72 item != &*vanilla_items::AIR,
73 "Item stack template item must be non-empty"
74 );
75 Self {
76 item,
77 count: 1,
78 components: DataComponentPatch::new(),
79 components_hash: Some(empty_map_hash()),
80 }
81 }
82
83 #[must_use]
85 pub fn with_count(item: ItemRef, count: i32) -> Self {
86 assert!(
87 (Self::MIN_COUNT..=Self::MAX_COUNT).contains(&count),
88 "Item stack template count {count} is outside the persistent range {}..={}",
89 Self::MIN_COUNT,
90 Self::MAX_COUNT
91 );
92 let mut template = Self::new(item);
93 template.count = count;
94 template
95 }
96
97 pub fn try_with_count_and_patch(
99 item: ItemRef,
100 count: i32,
101 components: DataComponentPatch,
102 ) -> Result<Self> {
103 if item == &*vanilla_items::AIR {
104 return Err(Error::other("Item stack template item must be non-empty"));
105 }
106 if !(Self::MIN_COUNT..=Self::MAX_COUNT).contains(&count) {
107 return Err(Error::other(format!(
108 "Item stack template count {count} is outside the persistent range {}..={}",
109 Self::MIN_COUNT,
110 Self::MAX_COUNT
111 )));
112 }
113 components.try_to_nbt_tag_ref()?;
114 let components_hash = components.compute_persistent_hash()?;
115 Ok(Self {
116 item,
117 count,
118 components,
119 components_hash: Some(components_hash),
120 })
121 }
122
123 pub(crate) fn from_extracted(
129 item: ItemRef,
130 count: i32,
131 components: DataComponentPatch,
132 components_hash: i32,
133 ) -> Self {
134 assert!(
135 item != &*vanilla_items::AIR,
136 "Extracted recipe result must be non-empty"
137 );
138 assert!(
139 (Self::MIN_COUNT..=Self::MAX_COUNT).contains(&count),
140 "Extracted recipe result count is outside the persistent range"
141 );
142 Self {
143 item,
144 count,
145 components,
146 components_hash: Some(components_hash),
147 }
148 }
149
150 pub fn from_stack(stack: &ItemStack) -> Result<Self> {
152 if stack.is_empty() {
153 return Err(Error::other("Stack must be non-empty"));
154 }
155 Self::try_with_count_and_patch(stack.item, stack.count, stack.components_patch().clone())
156 }
157
158 pub(crate) fn validate_persistent_encoding(&self) -> Result<()> {
159 if self.item == &*vanilla_items::AIR
160 || !(Self::MIN_COUNT..=Self::MAX_COUNT).contains(&self.count)
161 {
162 return Err(Error::other("Item stack template is not persistable"));
163 }
164 self.components.try_to_nbt_tag_ref().map(|_| ())
165 }
166
167 #[must_use]
168 pub const fn item(&self) -> ItemRef {
169 self.item
170 }
171
172 #[must_use]
173 pub const fn count(&self) -> i32 {
174 self.count
175 }
176
177 #[must_use]
178 pub const fn components(&self) -> &DataComponentPatch {
179 &self.components
180 }
181
182 #[must_use]
183 pub fn create(&self) -> ItemStack {
184 let result =
185 ItemStack::with_count_and_patch(self.item, self.count, self.components.clone());
186 if let Err(error) = result.validate_strict() {
187 log::warn!("Can't create item stack with properties {self:?}, error: {error}");
188 return ItemStack::empty();
189 }
190 result
191 }
192
193 #[must_use]
196 pub fn apply(&self, count: i32, additional_components: &DataComponentPatch) -> ItemStack {
197 let mut components = additional_components.clone();
198 components.apply(&self.components);
199 let result = ItemStack::with_count_and_patch(self.item, count, components);
200 if let Err(error) = result.validate_strict() {
201 log::warn!("Can't create item stack with properties {self:?}, error: {error}");
202 return ItemStack::empty();
203 }
204 result
205 }
206
207 pub fn to_hover_event(&self) -> Result<HoverEvent> {
209 let components = if self.components.is_empty() {
210 None
211 } else {
212 Some(EncodedNbt::encode(&self.components)?)
213 };
214 Ok(HoverEvent::show_item(
215 self.item.key.to_string(),
216 Some(self.count),
217 components,
218 ))
219 }
220
221 pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
222 let mut compound = NbtCompound::new();
223 compound.insert("id", self.item.key.to_string());
224 if self.count != 1 {
225 compound.insert("count", self.count);
226 }
227 if !self.components.is_empty() {
228 compound.insert("components", self.components.to_nbt_tag_ref());
229 }
230 NbtTag::Compound(compound)
231 }
232
233 pub(crate) fn from_nbt_identifier(value: &str) -> Option<Self> {
234 let _depth = TemplateDepthGuard::enter().ok()?;
235 let key = Identifier::from_str(value).ok()?;
236 let item = REGISTRY.items.by_key(&key)?;
237 (item != &*vanilla_items::AIR).then(|| Self::new(item))
238 }
239
240 pub(crate) fn from_nbt_compound(
241 compound: simdnbt::borrow::NbtCompound<'_, '_>,
242 ) -> Option<Self> {
243 let _depth = TemplateDepthGuard::enter().ok()?;
244 let key = Identifier::from_str(&compound.get("id")?.string()?.to_str()).ok()?;
245 let item = REGISTRY.items.by_key(&key)?;
246 let count = match compound.get("count") {
247 Some(count) => count.codec_i32()?,
248 None => 1,
249 };
250 let components = match compound.get("components") {
251 Some(components) => DataComponentPatch::from_nbt_tag(components)?,
252 None => DataComponentPatch::new(),
253 };
254 Self::try_with_count_and_patch(item, count, components).ok()
255 }
256
257 fn from_stream(item: ItemRef, count: i32, components: DataComponentPatch) -> Result<Self> {
258 if item == &*vanilla_items::AIR || count == 0 {
259 return Err(Error::other("Item stack template must be non-empty"));
260 }
261 let components_hash = components.compute_persistent_hash().ok();
262 Ok(Self {
263 item,
264 count,
265 components,
266 components_hash,
267 })
268 }
269
270 #[must_use]
272 pub fn get_effective_value_raw(&self, key: &Identifier) -> Option<&ComponentData> {
273 match self.components.get_entry(key) {
274 Some(ComponentPatchEntry::Set(value)) => Some(value),
275 Some(ComponentPatchEntry::Removed) => None,
276 None => self.item.components.get_raw(key),
277 }
278 }
279
280 #[must_use]
282 pub fn get<T: Component + DowncastType>(&self, component: DataComponentType<T>) -> Option<&T> {
283 self.get_effective_value_raw(&component.key)
284 .and_then(ComponentData::downcast_ref::<T>)
285 }
286
287 pub(crate) fn max_stack_size(&self) -> i32 {
288 self.get(MAX_STACK_SIZE).copied().unwrap_or(1)
289 }
290}
291
292impl WriteTo for ItemStackTemplate {
293 fn write(&self, writer: &mut impl Write) -> Result<()> {
294 let _depth = TemplateDepthGuard::enter()?;
295 let item_id = i32::try_from(self.item.id())
296 .map_err(|_| Error::other(format!("Item id is too large: {}", self.item.id())))?;
297 VarInt(item_id).write(writer)?;
298 VarInt(self.count).write(writer)?;
299 self.components.write(writer)
300 }
301}
302
303impl ReadFrom for ItemStackTemplate {
304 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
305 let _depth = TemplateDepthGuard::enter()?;
306 let item_id = VarInt::read(data)?.0;
307 let item_id = usize::try_from(item_id)
308 .map_err(|_| Error::other(format!("Negative item id: {item_id}")))?;
309 let item = REGISTRY
310 .items
311 .by_id(item_id)
312 .ok_or_else(|| Error::other(format!("Unknown item id: {item_id}")))?;
313 let count = VarInt::read(data)?.0;
314 let components = DataComponentPatch::read(data)?;
315 Self::from_stream(item, count, components)
316 }
317}
318
319impl ToNbtTag for ItemStackTemplate {
320 fn to_nbt_tag(self) -> NbtTag {
321 self.to_nbt_tag_ref()
322 }
323}
324
325impl FromNbtTag for ItemStackTemplate {
326 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
327 if let Some(value) = tag.string() {
328 return Self::from_nbt_identifier(&value.to_str());
329 }
330 Self::from_nbt_compound(tag.compound()?)
331 }
332}
333
334impl HashComponent for ItemStackTemplate {
335 fn hash_component(&self, hasher: &mut ComponentHasher) {
336 let mut entries = Vec::with_capacity(3);
337 push_hash_entry(&mut entries, "id", self.item.key.to_string().compute_hash());
338 if self.count != 1 {
339 push_hash_entry(&mut entries, "count", self.count.compute_hash());
340 }
341 if !self.components.is_empty() {
342 let Some(components_hash) = self.components_hash else {
343 panic!("stream-only item stack template must validate before persistent hashing");
344 };
345 push_hash_entry(&mut entries, "components", components_hash);
346 }
347 sort_map_entries(&mut entries);
348 hasher.start_map();
349 for entry in &entries {
350 hasher.put_raw_bytes(&entry.key_bytes);
351 hasher.put_raw_bytes(&entry.value_bytes);
352 }
353 hasher.end_map();
354 }
355}
356
357fn empty_map_hash() -> i32 {
358 let mut hasher = ComponentHasher::new();
359 hasher.start_map();
360 hasher.end_map();
361 hasher.finish()
362}
363
364fn push_hash_entry(entries: &mut Vec<HashEntry>, key: &str, value_hash: i32) {
365 let key_hash = key.compute_hash() as u32;
366 let value_hash = value_hash as u32;
367 entries.push(HashEntry {
368 key_hash: i64::from(key_hash),
369 value_hash: i64::from(value_hash),
370 key_bytes: key_hash.to_le_bytes(),
371 value_bytes: value_hash.to_le_bytes(),
372 });
373}
374
375#[cfg(test)]
376mod tests {
377 use std::io::Cursor;
378
379 use simdnbt::borrow::read_tag;
380 use simdnbt::owned::{NbtCompound, NbtTag};
381 use simdnbt::{FromNbtTag as _, ToNbtTag as _};
382 use steel_utils::hash::HashComponent as _;
383 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
384
385 use super::ItemStackTemplate;
386 use crate::RegistryEntry as _;
387 use crate::data_components::components::{
388 BundleContents, ChargedProjectiles, ItemContainerContents,
389 };
390 use crate::data_components::vanilla_components::{
391 BUNDLE_CONTENTS, CHARGED_PROJECTILES, CONTAINER, CUSTOM_NAME, ENCHANTMENT_GLINT_OVERRIDE,
392 MAX_DAMAGE, MAX_STACK_SIZE,
393 };
394 use crate::data_components::{ComponentData, DataComponentPatch};
395 use crate::init_vanilla_registry;
396 use crate::vanilla_items;
397 use crate::{REGISTRY, RegistryExt as _};
398 use text_components::{Modifier as _, TextComponent};
399
400 fn parse(tag: NbtTag) -> Option<ItemStackTemplate> {
401 let mut bytes = Vec::new();
402 tag.write(&mut bytes);
403 let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
404 ItemStackTemplate::from_nbt_tag(borrowed.as_tag())
405 }
406
407 #[test]
408 fn item_only_alternative_decodes_and_primary_codec_omits_defaults() {
409 init_vanilla_registry();
410 let template = ItemStackTemplate::new(&vanilla_items::STICK);
411 let mut expected = NbtCompound::new();
412 expected.insert("id", "minecraft:stick");
413 let expected = NbtTag::Compound(expected);
414 assert_eq!(template.clone().to_nbt_tag(), expected);
415 assert_eq!(template.compute_hash(), expected.compute_hash());
416 assert_eq!(parse(NbtTag::String("stick".into())), Some(template));
417 }
418
419 #[test]
420 fn complete_templates_round_trip_both_codecs() {
421 init_vanilla_registry();
422 let mut patch = DataComponentPatch::new();
423 patch.set(ENCHANTMENT_GLINT_OVERRIDE, true);
424 let template =
425 ItemStackTemplate::try_with_count_and_patch(&vanilla_items::DIAMOND, 3, patch)
426 .expect("valid template should construct");
427 assert_eq!(parse(template.clone().to_nbt_tag()), Some(template.clone()));
428
429 let mut network = Vec::new();
430 template
431 .write(&mut network)
432 .expect("template should encode");
433 let decoded = ItemStackTemplate::read(&mut Cursor::new(network.as_slice()))
434 .expect("template should decode");
435 assert_eq!(decoded, template);
436 assert_eq!(decoded.compute_hash(), template.compute_hash());
437 }
438
439 #[test]
440 fn hover_events_embed_the_typed_component_patch_codec_output() {
441 init_vanilla_registry();
442 let mut patch = DataComponentPatch::new();
443 patch.set(CUSTOM_NAME, TextComponent::plain("Stone"));
444 let template = ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 2, patch)
445 .expect("valid template should construct");
446 let component = TextComponent::plain("item").hover_event(
447 template
448 .to_hover_event()
449 .expect("valid template should encode for a hover event"),
450 );
451
452 let NbtTag::Compound(component) = component.to_codec_nbt() else {
453 panic!("hover component should encode as a compound");
454 };
455 let components = component
456 .get("hover_event")
457 .and_then(NbtTag::compound)
458 .and_then(|hover| hover.get("components"))
459 .and_then(NbtTag::compound)
460 .expect("hover event should contain a component patch");
461 assert_eq!(
462 components.get("minecraft:custom_name"),
463 Some(&NbtTag::String("Stone".into()))
464 );
465 }
466
467 #[test]
468 fn template_invariants_reject_empty_items_and_unpersistable_counts() {
469 init_vanilla_registry();
470 for count in [-1, 0, 100] {
471 assert!(
472 ItemStackTemplate::try_with_count_and_patch(
473 &vanilla_items::STICK,
474 count,
475 DataComponentPatch::new(),
476 )
477 .is_err()
478 );
479 }
480 assert!(
481 ItemStackTemplate::try_with_count_and_patch(
482 &vanilla_items::AIR,
483 1,
484 DataComponentPatch::new(),
485 )
486 .is_err()
487 );
488 }
489
490 #[test]
491 fn stream_codec_accepts_nonzero_counts_outside_persistent_range() {
492 init_vanilla_registry();
493 for count in [-1, 100] {
494 let mut encoded = Vec::new();
495 steel_utils::codec::VarInt(vanilla_items::STICK.id() as i32)
496 .write(&mut encoded)
497 .expect("item id should encode");
498 steel_utils::codec::VarInt(count)
499 .write(&mut encoded)
500 .expect("count should encode");
501 DataComponentPatch::new()
502 .write(&mut encoded)
503 .expect("patch should encode");
504 let decoded = ItemStackTemplate::read(&mut Cursor::new(encoded.as_slice()))
505 .expect("nonzero stream count should decode");
506 assert_eq!(decoded.count(), count);
507 }
508 }
509
510 #[test]
511 fn containing_component_hash_rejects_stream_only_nested_patch() {
512 init_vanilla_registry();
513 let mut patch = DataComponentPatch::new();
514 patch.set(MAX_STACK_SIZE, 0);
515 let template = ItemStackTemplate::from_stream(&vanilla_items::STONE, 1, patch)
516 .expect("stream codec should admit the nested patch");
517 let bundle = BundleContents::new(vec![template]);
518 let entry = REGISTRY
519 .data_components
520 .by_key(BUNDLE_CONTENTS.key())
521 .expect("bundle_contents should be registered");
522 assert!(entry.compute_hash(&ComponentData::new(bundle)).is_err());
523 }
524
525 #[test]
526 fn item_only_air_alternative_returns_codec_failure() {
527 init_vanilla_registry();
528
529 assert!(parse(NbtTag::String("minecraft:air".into())).is_none());
530 }
531
532 #[test]
533 fn create_rejects_invalid_effective_stack_constraints() {
534 init_vanilla_registry();
535
536 let mut oversized_patch = DataComponentPatch::new();
537 oversized_patch.set(MAX_STACK_SIZE, 1);
538 let oversized =
539 ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 2, oversized_patch)
540 .expect("template codec permits counts above the effective stack maximum");
541 assert!(oversized.create().is_empty());
542
543 let mut damageable_patch = DataComponentPatch::new();
544 damageable_patch.set(MAX_DAMAGE, 1);
545 let damageable =
546 ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, damageable_patch)
547 .expect("individually valid components should construct a template");
548 assert!(damageable.create().is_empty());
549
550 assert!(
551 !ItemStackTemplate::new(&vanilla_items::STONE)
552 .create()
553 .is_empty()
554 );
555 }
556
557 #[test]
558 fn create_rejects_oversized_recursive_contents() {
559 init_vanilla_registry();
560
561 let mut container_patch = DataComponentPatch::new();
562 container_patch.set(
563 CONTAINER,
564 ItemContainerContents::new(vec![Some(oversized_stone_template())])
565 .expect("one container slot should be valid"),
566 );
567 let container =
568 ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, container_patch)
569 .expect("container component should persist");
570 assert!(container.create().is_empty());
571
572 let mut bundle_patch = DataComponentPatch::new();
573 bundle_patch.set(
574 BUNDLE_CONTENTS,
575 BundleContents::new(vec![oversized_stone_template()]),
576 );
577 let bundle =
578 ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, bundle_patch)
579 .expect("bundle component should persist");
580 assert!(bundle.create().is_empty());
581
582 let mut projectile_patch = DataComponentPatch::new();
583 projectile_patch.set(
584 CHARGED_PROJECTILES,
585 ChargedProjectiles::new(vec![oversized_stone_template()])
586 .expect("one charged projectile should be valid"),
587 );
588 let projectiles =
589 ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, projectile_patch)
590 .expect("charged-projectiles component should persist");
591 assert!(projectiles.create().is_empty());
592 }
593
594 #[test]
595 fn create_rejects_excessive_bundle_weight_arithmetic() {
596 init_vanilla_registry();
597
598 let items = [97, 89, 83, 79, 73]
599 .into_iter()
600 .map(|max_stack_size| {
601 let mut patch = DataComponentPatch::new();
602 patch.set(MAX_STACK_SIZE, max_stack_size);
603 ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, patch)
604 .expect("prime max stack size should be persistable")
605 })
606 .collect();
607 let mut patch = DataComponentPatch::new();
608 patch.set(BUNDLE_CONTENTS, BundleContents::new(items));
609 let template = ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, patch)
610 .expect("bundle with individually valid entries should persist");
611
612 assert!(template.create().is_empty());
613 }
614
615 fn oversized_stone_template() -> ItemStackTemplate {
616 let mut patch = DataComponentPatch::new();
617 patch.set(MAX_STACK_SIZE, 1);
618 ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 2, patch)
619 .expect("template codec permits counts above the effective stack maximum")
620 }
621}