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
21pub struct Item {
23 pub key: Identifier,
24 pub components: DataComponentMap,
25 pub block: Option<BlockRef>,
27 pub craft_remainder: Option<Identifier>,
30 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 #[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 #[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 #[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 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
121unsafe 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 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 #[must_use]
175 pub const fn is_block_item(&self, item: ItemRef) -> bool {
176 item.block.is_some()
177 }
178
179 #[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}