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, 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 block held by vanilla `BlockItem`
26    pub block: Option<BlockRef>,
27    /// The item key returned when this item is used in crafting (e.g., "bucket" from `milk_bucket`).
28    /// Stored as an Identifier to avoid circular reference issues during initialization.
29    pub craft_remainder: Option<Identifier>,
30    /// Cached registry ID, set during registration for O(1) lookup on hot paths.
31    pub id: OnceLock<usize>,
32}
33
34impl std::fmt::Debug for Item {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("Item").field("key", &self.key).finish()
37    }
38}
39
40impl Item {
41    /// Creates an item with Vanilla's mandatory name and default model components.
42    #[must_use]
43    pub fn new(
44        key: Identifier,
45        item_name: TextComponent,
46        craft_remainder: Option<Identifier>,
47    ) -> Self {
48        let mut components = DataComponentMap::common_item_components();
49        components.set(ITEM_NAME, Some(item_name));
50        components.set(ITEM_MODEL, Some(key.clone()));
51        Self {
52            key,
53            components,
54            block: None,
55            craft_remainder,
56            id: OnceLock::new(),
57        }
58    }
59
60    #[must_use]
61    pub fn from_block(block: BlockRef, item_name: TextComponent) -> Self {
62        let mut item = Self::new(block.key.clone(), item_name, None);
63        item.block = Some(block);
64        item
65    }
66
67    #[must_use]
68    pub fn from_block_custom_name(
69        block: BlockRef,
70        name: &'static str,
71        item_name: TextComponent,
72    ) -> Self {
73        let mut item = Self::new(Identifier::vanilla_static(name), item_name, None);
74        item.block = Some(block);
75        item
76    }
77
78    /// Builder method to set a component on this item. Used during static initialization.
79    #[must_use]
80    pub fn builder_set<T: crate::data_components::Component + DowncastType>(
81        mut self,
82        component: crate::data_components::DataComponentType<T>,
83        value: Option<T>,
84    ) -> Self {
85        self.components.set(component, value);
86        self
87    }
88
89    /// Returns the item stack that remains after this item is used in crafting.
90    /// For example, `milk_bucket` returns an empty bucket.
91    #[must_use]
92    pub fn get_crafting_remainder(&self) -> ItemStack {
93        match &self.craft_remainder {
94            Some(remainder_key) => {
95                if let Some(remainder_item) = REGISTRY.items.by_key(remainder_key) {
96                    ItemStack::new(remainder_item)
97                } else {
98                    ItemStack::empty()
99                }
100            }
101            None => ItemStack::empty(),
102        }
103    }
104
105    /// Returns `true` if this item is tagged with the given tag.
106    pub fn has_tag(&'static self, tag: &Identifier) -> bool {
107        REGISTRY.items.is_in_tag(self, tag)
108    }
109}
110
111pub type ItemRef = &'static Item;
112
113pub struct ItemRegistry {
114    items_by_id: Vec<ItemRef>,
115    items_by_key: FxHashMap<Identifier, usize>,
116    items_by_block: FxHashMap<Identifier, usize>,
117    tags: RegistryTags,
118    allows_registering: bool,
119}
120
121// SAFETY: This Steel-owned key uniquely identifies the item registry.
122unsafe impl DowncastType for ItemRegistry {
123    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:registry/item");
124}
125
126impl Default for ItemRegistry {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132impl ItemRegistry {
133    #[must_use]
134    pub fn new() -> Self {
135        Self {
136            items_by_id: Vec::new(),
137            items_by_key: FxHashMap::default(),
138            items_by_block: FxHashMap::default(),
139            tags: RegistryTags::default(),
140            allows_registering: true,
141        }
142    }
143
144    pub fn register(&mut self, item: ItemRef) -> usize {
145        assert!(
146            self.allows_registering,
147            "Cannot register items after the registry has been frozen"
148        );
149
150        let id = self.items_by_id.len();
151        let cached = item.id.get_or_init(|| id);
152        assert_eq!(*cached, id, "item registered with conflicting id");
153        self.items_by_key.insert(item.key.clone(), id);
154        self.items_by_id.push(item);
155
156        id
157    }
158
159    /// Registers the vanilla `BlockItem` association used by `Block.asItem()`.
160    pub fn register_block_item(&mut self, block: BlockRef, item: ItemRef) {
161        assert!(
162            self.allows_registering,
163            "Cannot register block items after the registry has been frozen"
164        );
165        let Some(&item_id) = self.items_by_key.get(&item.key) else {
166            panic!("Cannot associate an unregistered item with a block");
167        };
168        self.items_by_block.insert(block.key.clone(), item_id);
169    }
170
171    /// Returns whether this item is Vanilla's `BlockItem` or one of its subclasses.
172    ///
173    /// The extracted blockItem target identifies the class capability
174    #[must_use]
175    pub const fn is_block_item(&self, item: ItemRef) -> bool {
176        item.block.is_some()
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}