Skip to main content

steel_core/entity/entities/objects/vehicles/
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::nbt::NbtValueInput as _;
16use steel_utils::{DowncastType, DowncastTypeKey};
17
18use super::abstract_minecart::minecart_relative_portal_position;
19use super::vehicle_entity::{VEHICLE_DIMENSION_CHANGING_DELAY, vehicle_ignores_explosion};
20use crate::entity::{Entity, EntityBase, EntityBaseLoad, EntityMovementEmission};
21use crate::world::{Explosion, 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 movement_emission(&self) -> EntityMovementEmission {
100        EntityMovementEmission::Events
101    }
102
103    fn is_pickable(&self) -> bool {
104        !self.is_removed()
105    }
106
107    fn is_pushable(&self) -> bool {
108        true
109    }
110
111    fn blocks_building(&self) -> bool {
112        true
113    }
114
115    fn ignore_explosion(&self, explosion: &dyn Explosion) -> bool {
116        vehicle_ignores_explosion(explosion)
117    }
118
119    fn dimension_changing_delay(&self) -> i32 {
120        VEHICLE_DIMENSION_CHANGING_DELAY
121    }
122
123    fn get_relative_portal_position(&self, axis: Axis, portal_area: FoundRectangle) -> DVec3 {
124        minecart_relative_portal_position(self, axis, portal_area)
125    }
126
127    fn save_additional(&self, nbt: &mut NbtCompound) {
128        nbt.insert("FlippedRotation", Self::nbt_bool(false));
129        let state = self.state.lock();
130        nbt.insert("HasTicked", Self::nbt_bool(state.first_tick));
131
132        if let Some(loot_table) = state.loot_table.as_ref() {
133            nbt.insert("LootTable", loot_table.to_string());
134            if state.loot_table_seed != 0 {
135                nbt.insert("LootTableSeed", NbtTag::Long(state.loot_table_seed));
136            }
137        }
138    }
139
140    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
141        let loot_table = nbt
142            .string("LootTable")
143            .and_then(|value| Identifier::from_str(&value.to_string()).ok());
144        let mut state = self.state.lock();
145        state.first_tick = nbt.byte_value("HasTicked").is_some_and(|value| value != 0);
146        state.loot_table = loot_table;
147        state.loot_table_seed = nbt.long_value("LootTableSeed").unwrap_or(0);
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use steel_registry::vanilla_entities;
155
156    #[test]
157    fn chest_minecart_saves_structure_loot_table_state() {
158        let minecart = ChestMinecartEntity::new(
159            &vanilla_entities::CHEST_MINECART,
160            1,
161            DVec3::new(1.5, 2.5, 3.5),
162            Weak::new(),
163        );
164        minecart.set_loot_table(
165            Identifier::new_static("minecraft", "chests/abandoned_mineshaft"),
166            42,
167        );
168
169        let mut nbt = NbtCompound::new();
170        minecart.save_additional(&mut nbt);
171
172        assert_eq!(
173            nbt.string("LootTable").map(ToString::to_string),
174            Some("minecraft:chests/abandoned_mineshaft".to_owned())
175        );
176        assert_eq!(nbt.long("LootTableSeed"), Some(42));
177        assert_eq!(nbt.byte("HasTicked"), Some(1));
178        assert_eq!(nbt.byte("FlippedRotation"), Some(0));
179    }
180
181    #[test]
182    fn chest_minecart_is_pickable_and_pushable_like_vanilla() {
183        let minecart = ChestMinecartEntity::new(
184            &vanilla_entities::CHEST_MINECART,
185            1,
186            DVec3::new(1.5, 2.5, 3.5),
187            Weak::new(),
188        );
189
190        assert!(minecart.is_pickable());
191        assert!(minecart.is_pushable());
192        assert!(minecart.blocks_building());
193    }
194
195    #[test]
196    fn chest_minecart_relative_portal_position_resets_forward_offset() {
197        let minecart = ChestMinecartEntity::new(
198            &vanilla_entities::CHEST_MINECART,
199            1,
200            DVec3::new(12.0, 66.0, 20.75),
201            Weak::new(),
202        );
203        let portal_area = FoundRectangle {
204            min_corner: steel_utils::BlockPos::new(10, 64, 20),
205            axis1_size: 4,
206            axis2_size: 5,
207        };
208
209        assert!(
210            minecart
211                .get_relative_portal_position(Axis::X, portal_area)
212                .z
213                .abs()
214                < f64::EPSILON
215        );
216    }
217}