Skip to main content

steel_core/block_entity/entities/
unimplemented.rs

1//! NBT-preserving fallback block entity.
2
3use std::sync::Weak;
4
5use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
6use simdnbt::owned::NbtCompound;
7use steel_registry::block_entity_type::BlockEntityTypeRef;
8use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey, locks::SyncMutex};
9
10use crate::block_entity::{BlockEntity, BlockEntityBase};
11use crate::world::World;
12
13struct UnimplementedBlockEntityState {
14    data: NbtCompound,
15}
16
17/// Steel-specific fallback for block entity types whose runtime behavior is not implemented yet.
18///
19/// Vanilla has concrete classes for every block entity type. Steel uses this only to preserve
20/// worldgen and disk NBT until the corresponding typed implementation is added.
21pub struct UnimplementedBlockEntity {
22    base: BlockEntityBase,
23    state: SyncMutex<UnimplementedBlockEntityState>,
24}
25
26// SAFETY: This key identifies the Steel fallback implementation, independently
27// of the Minecraft block-entity registry entry stored inside it.
28unsafe impl DowncastType for UnimplementedBlockEntity {
29    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/unimplemented");
30}
31
32impl UnimplementedBlockEntity {
33    /// Creates an unimplemented block entity without additional NBT.
34    #[must_use]
35    pub fn new(
36        block_entity_type: BlockEntityTypeRef,
37        level: Weak<World>,
38        pos: BlockPos,
39        state: BlockStateId,
40    ) -> Self {
41        Self {
42            base: BlockEntityBase::new(block_entity_type, level, pos, state),
43            state: SyncMutex::new(UnimplementedBlockEntityState {
44                data: NbtCompound::new(),
45            }),
46        }
47    }
48
49    /// Loads an unimplemented block entity from owned NBT, including stored components.
50    pub fn with_data(
51        block_entity_type: BlockEntityTypeRef,
52        level: Weak<World>,
53        pos: BlockPos,
54        state: BlockStateId,
55        data: NbtCompound,
56    ) -> Result<Self, simdnbt::Error> {
57        let entity = Self::new(block_entity_type, level, pos, state);
58        entity.load_with_owned_components(&data)?;
59        Ok(entity)
60    }
61}
62
63impl BlockEntity for UnimplementedBlockEntity {
64    fn base(&self) -> &BlockEntityBase {
65        &self.base
66    }
67
68    fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
69        let nbt_view: NbtCompoundView<'_, '_> = nbt.into();
70        let mut data = nbt_view.to_owned();
71        while data.remove("components").is_some() {}
72        self.state.lock().data = data;
73    }
74
75    fn save_additional(&self, nbt: &mut NbtCompound) {
76        *nbt = self.state.lock().data.clone();
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use std::sync::Weak;
83
84    use steel_registry::data_components::{DataComponentMap, vanilla_components::REPAIR_COST};
85    use steel_registry::{init_vanilla_registry, vanilla_block_entity_types, vanilla_blocks};
86
87    use super::*;
88
89    #[test]
90    fn full_metadata_replaces_stale_unimplemented_metadata() {
91        init_vanilla_registry();
92        let mut data = NbtCompound::new();
93        data.insert("id", "minecraft:chest");
94        data.insert("x", 100_i32);
95        data.insert("custom", 7_i32);
96        let mut components = DataComponentMap::new();
97        components.set(REPAIR_COST, Some(3));
98        data.insert("components", components.to_nbt_tag_ref());
99        let entity = UnimplementedBlockEntity::with_data(
100            &vanilla_block_entity_types::BARREL,
101            Weak::new(),
102            BlockPos::new(2, 70, -4),
103            vanilla_blocks::BARREL.default_state(),
104            data,
105        )
106        .expect("fallback block entity should load owned NBT");
107
108        let saved = entity.save_with_full_metadata();
109        let custom = entity.save_custom_only();
110
111        assert_eq!(
112            saved.string("id").map(ToString::to_string),
113            Some("minecraft:barrel".to_owned())
114        );
115        assert_eq!(saved.int("x"), Some(2));
116        assert_eq!(saved.int("y"), Some(70));
117        assert_eq!(saved.int("z"), Some(-4));
118        assert_eq!(saved.int("custom"), Some(7));
119        assert_eq!(entity.collect_components().get(REPAIR_COST), Some(3));
120        assert!(!custom.contains("id"));
121        assert!(!custom.contains("x"));
122        assert!(!custom.contains("components"));
123        assert_eq!(custom.int("custom"), Some(7));
124    }
125
126    #[test]
127    #[should_panic(expected = "invalid block entity minecraft:barrel state minecraft:stone")]
128    fn constructor_rejects_a_type_state_mismatch() {
129        init_vanilla_registry();
130        let _ = UnimplementedBlockEntity::new(
131            &vanilla_block_entity_types::BARREL,
132            Weak::new(),
133            BlockPos::new(2, 70, -4),
134            vanilla_blocks::STONE.default_state(),
135        );
136    }
137}