Skip to main content

steel_core/block_entity/entities/
enchanting_table.rs

1//! Enchanting table block entity.
2
3use std::sync::{Arc, Weak};
4
5use simdnbt::borrow::{
6    BaseNbtCompound as BorrowedNbtCompound, NbtCompound as BorrowedNbtCompoundView,
7};
8use simdnbt::owned::NbtCompound;
9use steel_registry::data_components::{DataComponentMap, vanilla_components::CUSTOM_NAME};
10use steel_registry::vanilla_block_entity_types;
11use steel_utils::{
12    BlockPos, BlockStateId, DowncastType, DowncastTypeKey, locks::SyncMutex, translations,
13};
14use text_components::TextComponent;
15
16use crate::block_entity::{BlockEntity, BlockEntityBase, ImplicitComponentGetter};
17use crate::world::World;
18
19/// Enchanting table block entity: holds the optional custom name.
20pub struct EnchantingTableBlockEntity {
21    base: Arc<BlockEntityBase>,
22    name: SyncMutex<Option<TextComponent>>,
23}
24
25// SAFETY: This key is owned by Steel and uniquely identifies `EnchantingTableBlockEntity`.
26unsafe impl DowncastType for EnchantingTableBlockEntity {
27    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/enchanting_table");
28}
29
30impl EnchantingTableBlockEntity {
31    /// Creates a new enchanting table block entity.
32    #[must_use]
33    pub fn new(level: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
34        Self {
35            base: Arc::new(BlockEntityBase::new(
36                &vanilla_block_entity_types::ENCHANTING_TABLE,
37                level,
38                pos,
39                state,
40            )),
41            name: SyncMutex::new(None),
42        }
43    }
44
45    /// Returns the custom name or the default enchanting table name
46    #[must_use]
47    pub fn display_name(&self) -> TextComponent {
48        self.name
49            .lock()
50            .clone()
51            .unwrap_or_else(|| TextComponent::translated(translations::CONTAINER_ENCHANT.msg()))
52    }
53
54    /// Returns the custom name if set
55    #[must_use]
56    pub fn custom_name(&self) -> Option<TextComponent> {
57        self.name.lock().clone()
58    }
59
60    /// Sets the custom name
61    pub fn set_custom_name(&self, name: Option<TextComponent>) {
62        *self.name.lock() = name;
63    }
64}
65
66impl BlockEntity for EnchantingTableBlockEntity {
67    fn base(&self) -> &BlockEntityBase {
68        &self.base
69    }
70
71    fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
72        let nbt_view: BorrowedNbtCompoundView<'_, '_> = nbt.into();
73        *self.name.lock() = nbt_view
74            .get("CustomName")
75            .and_then(|tag| TextComponent::from_nbt(&tag.to_owned()));
76    }
77
78    fn save_additional(&self, nbt: &mut NbtCompound) {
79        if let Some(name) = self.name.lock().as_ref() {
80            nbt.insert("CustomName", name.to_codec_nbt());
81        }
82    }
83
84    fn apply_implicit_components(&self, components: &mut ImplicitComponentGetter<'_>) {
85        *self.name.lock() = components.get(CUSTOM_NAME);
86    }
87
88    fn collect_implicit_components(&self, components: &mut DataComponentMap) {
89        components.set(CUSTOM_NAME, self.custom_name());
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use simdnbt::owned::NbtCompound;
96    use steel_registry::data_components::components::{BlockEntityData, CustomData};
97    use steel_registry::data_components::vanilla_components::{
98        BLOCK_ENTITY_DATA, CUSTOM_NAME, REPAIR_COST,
99    };
100    use steel_registry::item_stack::ItemStack;
101    use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_items};
102
103    use super::*;
104    use crate::test_support::fresh_test_world;
105
106    fn table(world: &Arc<World>) -> EnchantingTableBlockEntity {
107        EnchantingTableBlockEntity::new(
108            Arc::downgrade(world),
109            BlockPos::new(8, 64, 8),
110            vanilla_blocks::ENCHANTING_TABLE.default_state(),
111        )
112    }
113
114    #[test]
115    fn applying_item_components_consumes_the_custom_name_and_stores_the_rest() {
116        init_vanilla_registry();
117        let world_fixture = fresh_test_world("enchanting_table_apply_components");
118        let world = &world_fixture.world;
119        let table = table(world);
120        let name = TextComponent::from("Arcane Desk".to_string());
121        let mut stack = ItemStack::new(&vanilla_items::ENCHANTING_TABLE);
122        stack.set(CUSTOM_NAME, name.clone());
123        stack.set(REPAIR_COST, 3);
124
125        table.apply_components_from_item_stack(&stack);
126
127        assert_eq!(table.custom_name(), Some(name.clone()));
128        let stored = table.base().stored_components();
129        assert!(
130            !stored.has(CUSTOM_NAME),
131            "an implicit component must not also be stored"
132        );
133        assert_eq!(stored.get(REPAIR_COST), Some(3));
134        assert_eq!(table.collect_components().get(CUSTOM_NAME), Some(name));
135    }
136
137    #[test]
138    fn applying_item_components_skips_placement_only_components() {
139        init_vanilla_registry();
140        let world_fixture = fresh_test_world("enchanting_table_apply_placement_components");
141        let world = &world_fixture.world;
142        let table = table(world);
143        let Some(custom_data) = CustomData::try_from_compound(NbtCompound::new()) else {
144            panic!("an empty compound should be valid custom data");
145        };
146        let mut stack = ItemStack::new(&vanilla_items::ENCHANTING_TABLE);
147        stack.set(
148            BLOCK_ENTITY_DATA,
149            BlockEntityData::new(&vanilla_block_entity_types::ENCHANTING_TABLE, custom_data),
150        );
151
152        table.apply_components_from_item_stack(&stack);
153
154        assert!(table.base().stored_components().is_empty());
155        assert_eq!(table.custom_name(), None);
156    }
157
158    #[test]
159    fn applying_an_unnamed_item_clears_a_previous_custom_name() {
160        init_vanilla_registry();
161        let world_fixture = fresh_test_world("enchanting_table_apply_clears_name");
162        let world = &world_fixture.world;
163        let table = table(world);
164        table.set_custom_name(Some(TextComponent::from("Old".to_string())));
165
166        table.apply_components_from_item_stack(&ItemStack::new(&vanilla_items::ENCHANTING_TABLE));
167
168        assert_eq!(table.custom_name(), None);
169    }
170}