Skip to main content

steel_core/entity/entities/
chest_minecart.rs

1//! Chest minecart state needed by structure generation and persistence.
2
3use std::str::FromStr;
4use std::sync::Weak;
5
6use glam::DVec3;
7use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
8use simdnbt::owned::{NbtCompound, NbtTag};
9use steel_macros::entity_behavior;
10use steel_registry::entity_type::EntityTypeRef;
11use steel_utils::Identifier;
12use steel_utils::axis::Axis;
13use steel_utils::block_util::FoundRectangle;
14use steel_utils::locks::SyncMutex;
15use steel_utils::{DowncastType, DowncastTypeKey};
16
17use crate::entity::{
18    Entity, EntityBase, EntityBaseLoad, reset_forward_direction_of_relative_portal_position,
19};
20use crate::portal::portal_shape::PortalShape;
21use crate::world::World;
22
23/// Chest minecart entity state used by mineshaft generation.
24///
25/// Steel does not yet implement minecart movement or container interaction, so this
26/// entity currently preserves the vanilla placement and loot-table state that
27/// structure generation creates.
28#[entity_behavior(class = "MinecartChest")]
29pub struct ChestMinecartEntity {
30    base: EntityBase,
31    entity_type: EntityTypeRef,
32    state: SyncMutex<ChestMinecartState>,
33}
34
35// SAFETY: This key is owned by Steel and uniquely identifies `ChestMinecartEntity`.
36unsafe impl DowncastType for ChestMinecartEntity {
37    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/chest_minecart");
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41struct ChestMinecartState {
42    first_tick: bool,
43    loot_table: Option<Identifier>,
44    loot_table_seed: i64,
45}
46
47impl ChestMinecartState {
48    const fn new(first_tick: bool) -> Self {
49        Self {
50            first_tick,
51            loot_table: None,
52            loot_table_seed: 0,
53        }
54    }
55}
56
57impl ChestMinecartEntity {
58    /// Creates a new chest minecart entity.
59    #[must_use]
60    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
61        Self {
62            base: EntityBase::new(id, position, entity_type.dimensions, world),
63            entity_type,
64            state: SyncMutex::new(ChestMinecartState::new(true)),
65        }
66    }
67
68    /// Creates a chest minecart entity from saved data.
69    #[must_use]
70    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
71        Self {
72            base: EntityBase::from_load(load, entity_type.dimensions),
73            entity_type,
74            state: SyncMutex::new(ChestMinecartState::new(false)),
75        }
76    }
77
78    /// Sets the deferred loot table used when the container is first opened.
79    pub fn set_loot_table(&self, loot_table: Identifier, seed: i64) {
80        let mut state = self.state.lock();
81        state.loot_table = Some(loot_table);
82        state.loot_table_seed = seed;
83    }
84
85    const fn nbt_bool(value: bool) -> i8 {
86        if value { 1 } else { 0 }
87    }
88}
89
90impl Entity for ChestMinecartEntity {
91    fn base(&self) -> &EntityBase {
92        &self.base
93    }
94
95    fn entity_type(&self) -> EntityTypeRef {
96        self.entity_type
97    }
98
99    fn is_pickable(&self) -> bool {
100        !self.is_removed()
101    }
102
103    fn is_pushable(&self) -> bool {
104        true
105    }
106
107    fn blocks_building(&self) -> bool {
108        true
109    }
110
111    fn dimension_changing_delay(&self) -> i32 {
112        10
113    }
114
115    fn get_relative_portal_position(&self, axis: Axis, portal_area: FoundRectangle) -> DVec3 {
116        reset_forward_direction_of_relative_portal_position(PortalShape::get_relative_position(
117            portal_area,
118            axis,
119            self.position(),
120            self.dimensions_for_pose(self.pose()),
121        ))
122    }
123
124    fn save_additional(&self, nbt: &mut NbtCompound) {
125        nbt.insert("FlippedRotation", Self::nbt_bool(false));
126        let state = self.state.lock();
127        nbt.insert("HasTicked", Self::nbt_bool(state.first_tick));
128
129        if let Some(loot_table) = state.loot_table.as_ref() {
130            nbt.insert("LootTable", loot_table.to_string());
131            if state.loot_table_seed != 0 {
132                nbt.insert("LootTableSeed", NbtTag::Long(state.loot_table_seed));
133            }
134        }
135    }
136
137    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
138        let loot_table = nbt
139            .string("LootTable")
140            .and_then(|value| Identifier::from_str(&value.to_string()).ok());
141        let mut state = self.state.lock();
142        if let Some(first_tick) = nbt.byte("HasTicked") {
143            state.first_tick = first_tick != 0;
144        }
145        state.loot_table = loot_table;
146        state.loot_table_seed = nbt.long("LootTableSeed").unwrap_or(0);
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use steel_registry::vanilla_entities;
154
155    #[test]
156    fn chest_minecart_saves_structure_loot_table_state() {
157        let minecart = ChestMinecartEntity::new(
158            &vanilla_entities::CHEST_MINECART,
159            1,
160            DVec3::new(1.5, 2.5, 3.5),
161            Weak::new(),
162        );
163        minecart.set_loot_table(
164            Identifier::new_static("minecraft", "chests/abandoned_mineshaft"),
165            42,
166        );
167
168        let mut nbt = NbtCompound::new();
169        minecart.save_additional(&mut nbt);
170
171        assert_eq!(
172            nbt.string("LootTable").map(ToString::to_string),
173            Some("minecraft:chests/abandoned_mineshaft".to_owned())
174        );
175        assert_eq!(nbt.long("LootTableSeed"), Some(42));
176        assert_eq!(nbt.byte("HasTicked"), Some(1));
177        assert_eq!(nbt.byte("FlippedRotation"), Some(0));
178    }
179
180    #[test]
181    fn chest_minecart_is_pickable_and_pushable_like_vanilla() {
182        let minecart = ChestMinecartEntity::new(
183            &vanilla_entities::CHEST_MINECART,
184            1,
185            DVec3::new(1.5, 2.5, 3.5),
186            Weak::new(),
187        );
188
189        assert!(minecart.is_pickable());
190        assert!(minecart.is_pushable());
191        assert!(minecart.blocks_building());
192    }
193
194    #[test]
195    fn chest_minecart_relative_portal_position_resets_forward_offset() {
196        let minecart = ChestMinecartEntity::new(
197            &vanilla_entities::CHEST_MINECART,
198            1,
199            DVec3::new(12.0, 66.0, 20.75),
200            Weak::new(),
201        );
202        let portal_area = FoundRectangle {
203            min_corner: steel_utils::BlockPos::new(10, 64, 20),
204            axis1_size: 4,
205            axis2_size: 5,
206        };
207
208        assert!(
209            minecart
210                .get_relative_portal_position(Axis::X, portal_area)
211                .z
212                .abs()
213                < f64::EPSILON
214        );
215    }
216}