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
21pub struct Item {
23 pub key: Identifier,
24 pub components: DataComponentMap,
25 pub craft_remainder: Option<Identifier>,
28 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 #[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 #[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 #[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 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
115unsafe 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 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 #[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 #[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}