steel_core/entity/entities/objects/vehicles/
chest_minecart.rs1use 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#[entity_behavior(class = "MinecartChest")]
29pub struct ChestMinecartEntity {
30 base: EntityBase,
31 entity_type: EntityTypeRef,
32 state: SyncMutex<ChestMinecartState>,
33}
34
35unsafe 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 #[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 #[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 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}