Skip to main content

steel_registry/items/
mod.rs

1use std::sync::OnceLock;
2
3use rustc_hash::FxHashMap;
4
5use steel_utils::{DowncastType, DowncastTypeKey, Identifier};
6use text_components::TextComponent;
7
8pub mod item;
9
10use crate::{
11    REGISTRY, RegistryEntry, RegistryExt, RegistryTags, TaggedRegistryExt,
12    blocks::BlockRef,
13    data_components::{
14        DataComponentMap,
15        vanilla_components::{ITEM_MODEL, ITEM_NAME},
16    },
17    item_stack::ItemStack,
18    vanilla_items,
19};
20
21/// A Minecraft item type.
22pub struct Item {
23    pub key: Identifier,
24    pub components: DataComponentMap,
25    /// The item key returned when this item is used in crafting (e.g., "bucket" from `milk_bucket`).
26    /// Stored as an Identifier to avoid circular reference issues during initialization.
27    pub craft_remainder: Option<Identifier>,
28    /// Cached registry ID, set during registration for O(1) lookup on hot paths.
29    pub id: OnceLock<usize>,
30}
31
32impl std::fmt::Debug for Item {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.debug_struct("Item").field("key", &self.key).finish()
35    }
36}
37
38impl Item {
39    /// Creates an item with Vanilla's mandatory name and default model components.
40    #[must_use]
41    pub fn new(
42        key: Identifier,
43        item_name: TextComponent,
44        craft_remainder: Option<Identifier>,
45    ) -> Self {
46        let mut components = DataComponentMap::common_item_components();
47        components.set(ITEM_NAME, Some(item_name));
48        components.set(ITEM_MODEL, Some(key.clone()));
49        Self {
50            key,
51            components,
52            craft_remainder,
53            id: OnceLock::new(),
54        }
55    }
56
57    #[must_use]
58    pub fn from_block(block: BlockRef, item_name: TextComponent) -> Self {
59        Self::new(block.key.clone(), item_name, None)
60    }
61
62    #[must_use]
63    pub fn from_block_custom_name(
64        _block: BlockRef,
65        name: &'static str,
66        item_name: TextComponent,
67    ) -> Self {
68        Self::new(Identifier::vanilla_static(name), item_name, None)
69    }
70
71    /// Builder method to set a component on this item. Used during static initialization.
72    #[must_use]
73    pub fn builder_set<T: crate::data_components::Component + DowncastType>(
74        mut self,
75        component: crate::data_components::DataComponentType<T>,
76        value: Option<T>,
77    ) -> Self {
78        self.components.set(component, value);
79        self
80    }
81
82    /// Returns the item stack that remains after this item is used in crafting.
83    /// For example, `milk_bucket` returns an empty bucket.
84    #[must_use]
85    pub fn get_crafting_remainder(&self) -> ItemStack {
86        match &self.craft_remainder {
87            Some(remainder_key) => {
88                if let Some(remainder_item) = REGISTRY.items.by_key(remainder_key) {
89                    ItemStack::new(remainder_item)
90                } else {
91                    ItemStack::empty()
92                }
93            }
94            None => ItemStack::empty(),
95        }
96    }
97
98    /// Returns `true` if this item is tagged with the given tag.
99    pub fn has_tag(&'static self, tag: &Identifier) -> bool {
100        REGISTRY.items.is_in_tag(self, tag)
101    }
102}
103
104pub type ItemRef = &'static Item;
105
106pub struct ItemRegistry {
107    items_by_id: Vec<ItemRef>,
108    items_by_key: FxHashMap<Identifier, usize>,
109    items_by_block: FxHashMap<Identifier, usize>,
110    block_items_by_id: Vec<bool>,
111    tags: RegistryTags,
112    allows_registering: bool,
113}
114
115// SAFETY: This Steel-owned key uniquely identifies the item registry.
116unsafe impl DowncastType for ItemRegistry {
117    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:registry/item");
118}
119
120impl Default for ItemRegistry {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl ItemRegistry {
127    #[must_use]
128    pub fn new() -> Self {
129        Self {
130            items_by_id: Vec::new(),
131            items_by_key: FxHashMap::default(),
132            items_by_block: FxHashMap::default(),
133            block_items_by_id: Vec::new(),
134            tags: RegistryTags::default(),
135            allows_registering: true,
136        }
137    }
138
139    pub fn register(&mut self, item: ItemRef) -> usize {
140        assert!(
141            self.allows_registering,
142            "Cannot register items after the registry has been frozen"
143        );
144
145        let id = self.items_by_id.len();
146        let cached = item.id.get_or_init(|| id);
147        assert_eq!(*cached, id, "item registered with conflicting id");
148        self.items_by_key.insert(item.key.clone(), id);
149        self.items_by_id.push(item);
150        self.block_items_by_id.push(false);
151
152        id
153    }
154
155    /// Registers the vanilla `BlockItem` association used by `Block.asItem()`.
156    pub fn register_block_item(&mut self, block: BlockRef, item: ItemRef) {
157        assert!(
158            self.allows_registering,
159            "Cannot register block items after the registry has been frozen"
160        );
161        let Some(&item_id) = self.items_by_key.get(&item.key) else {
162            panic!("Cannot associate an unregistered item with a block");
163        };
164        self.items_by_block.insert(block.key.clone(), item_id);
165        self.block_items_by_id[item_id] = true;
166    }
167
168    /// Returns whether this item is Vanilla's `BlockItem` or one of its subclasses.
169    ///
170    /// `BlockItem` construction registers its block-to-item association, so the
171    /// extracted association is also the complete class-hierarchy capability.
172    #[must_use]
173    pub fn is_block_item(&self, item: ItemRef) -> bool {
174        self.block_items_by_id
175            .get(item.id())
176            .is_some_and(|&is_block_item| is_block_item)
177    }
178
179    /// Returns the item associated with this block, or air when it has no block item.
180    ///
181    /// Vanilla equivalent: `Item.byBlock(Block)` / `Block.asItem()`.
182    #[must_use]
183    pub fn by_block(&self, block: BlockRef) -> ItemRef {
184        self.items_by_block
185            .get(&block.key)
186            .map_or(&vanilla_items::AIR, |&item_id| self.items_by_id[item_id])
187    }
188
189    pub fn iter(&self) -> impl Iterator<Item = (usize, ItemRef)> + '_ {
190        self.items_by_id
191            .iter()
192            .enumerate()
193            .map(|(id, &item)| (id, item))
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use steel_utils::Identifier;
200    use text_components::TextComponent;
201
202    use super::Item;
203    use crate::{
204        REGISTRY,
205        data_components::vanilla_components::{ITEM_MODEL, ITEM_NAME},
206        init_vanilla_registry, vanilla_blocks, vanilla_items,
207    };
208
209    #[test]
210    fn new_item_uses_its_key_as_the_default_model() {
211        let key = Identifier::new_static("steel", "test_item");
212        let name = TextComponent::plain("Test Item");
213        let item = Item::new(key.clone(), name.clone(), None);
214
215        assert_eq!(item.components.get_ref(ITEM_MODEL), Some(&key));
216        assert_eq!(item.components.get_ref(ITEM_NAME), Some(&name));
217    }
218
219    #[test]
220    fn extracted_block_item_associations_match_vanilla() {
221        init_vanilla_registry();
222
223        assert_eq!(
224            REGISTRY.items.by_block(&vanilla_blocks::LEAF_LITTER),
225            &*vanilla_items::LEAF_LITTER
226        );
227        assert_eq!(
228            REGISTRY.items.by_block(&vanilla_blocks::REDSTONE_WIRE),
229            &*vanilla_items::REDSTONE
230        );
231        assert_eq!(
232            REGISTRY.items.by_block(&vanilla_blocks::WALL_TORCH),
233            &*vanilla_items::TORCH
234        );
235        assert_eq!(
236            REGISTRY.items.by_block(&vanilla_blocks::BIG_DRIPLEAF_STEM),
237            &*vanilla_items::BIG_DRIPLEAF
238        );
239        assert_eq!(
240            REGISTRY.items.by_block(&vanilla_blocks::FIRE),
241            &*vanilla_items::AIR
242        );
243    }
244
245    #[test]
246    fn block_item_capability_includes_extracted_vanilla_subclasses() {
247        init_vanilla_registry();
248
249        for item in [
250            &*vanilla_items::STONE,
251            &*vanilla_items::OAK_DOOR,
252            &*vanilla_items::OAK_SIGN,
253            &*vanilla_items::OAK_HANGING_SIGN,
254            &*vanilla_items::WHITE_BED,
255            &*vanilla_items::WHITE_BANNER,
256            &*vanilla_items::SCAFFOLDING,
257            &*vanilla_items::LILY_PAD,
258            &*vanilla_items::STRUCTURE_BLOCK,
259            &*vanilla_items::POWDER_SNOW_BUCKET,
260        ] {
261            assert!(REGISTRY.items.is_block_item(item));
262        }
263        assert!(
264            !REGISTRY
265                .items
266                .is_block_item(&vanilla_items::DIAMOND_PICKAXE)
267        );
268    }
269}
270
271crate::impl_registry_ext!(ItemRegistry, Item, items_by_id, items_by_key);
272crate::impl_tagged_registry!(ItemRegistry, items_by_key, "item");
273
274crate::impl_registry_entry_eq!(Item);
275
276impl crate::RegistryEntry for Item {
277    fn key(&self) -> &Identifier {
278        &self.key
279    }
280
281    fn try_id(&self) -> Option<usize> {
282        self.id.get().copied()
283    }
284}