steel_core/entity/entities/
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::{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#[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 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}