steel_core/entity/entities/objects/vehicles/
abstract_minecart.rs1use std::sync::Arc;
3
4use glam::DVec3;
5use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
6use simdnbt::owned::{NbtCompound, NbtTag};
7use steel_math::trig;
8use steel_registry::blocks::block_state_ext::BlockStateExt as _;
9use steel_registry::blocks::properties::{Direction, RailShape};
10use steel_registry::entity_type::EntityTypeRef;
11use steel_registry::item_stack::ItemStack;
12use steel_registry::vanilla_block_tags::BlockTag;
13use steel_registry::{vanilla_blocks, vanilla_entities};
14use steel_utils::axis::Axis;
15use steel_utils::block_util::FoundRectangle;
16use steel_utils::locks::SyncMutex;
17use steel_utils::nbt::NbtNumeric;
18use steel_utils::{BlockPos, BlockStateId};
19
20use super::minecart_behavior::MinecartBehavior;
21use super::vehicle_entity::VehicleEntity;
22use crate::block_entity::block_state_nbt;
23use crate::entity::reset_forward_direction_of_relative_portal_position;
24use crate::entity::{Entity, EntityMovementEmission};
25use crate::entity::{
26 EntitySpawnError, SharedEntity, apply_item_stack_components, create_entity_instance,
27};
28use crate::physics::{MoveResult, MoverType};
29use crate::player::Player;
30use crate::portal::portal_shape::PortalShape;
31use crate::world::{SignalGetter as _, World};
32
33const LOWERED_PASSENGER_ATTACHMENT: DVec3 = DVec3::ZERO;
34const WATER_SLOWDOWN_FACTOR: f32 = 0.95;
35const LAVA_FALL_DISTANCE_FACTOR: f64 = 0.5;
36const MIN_PUSH_DISTANCE_SQUARED: f32 = 1.0e-4;
37const MIN_COLLISION_ALIGNMENT: f32 = 0.8;
38const GRAVITY_IN_WATER: f64 = 0.005;
39const DEFAULT_GRAVITY: f64 = 0.04;
40const OFF_TRACK_GROUND_SLOWDOWN_FACTOR: f64 = 0.5;
41const PUSH_FORCE: f32 = 0.1;
42const COLLISION_VELOCITY_RETENTION: f64 = 0.2;
43const COLLISION_PUSH_SCALE: f64 = 0.5;
44const FURNACE_COLLISION_VELOCITY_RETENTION: f64 = 0.95;
45const NON_MINECART_PUSH_DIVISOR: f64 = 4.0;
46
47#[derive(Debug, Default)]
50pub struct AbstractMinecartBase {
51 state: SyncMutex<AbstractMinecartState>,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55struct AbstractMinecartState {
56 on_rails: bool,
57 flipped: bool,
58}
59
60impl AbstractMinecartBase {
61 #[must_use]
63 pub fn new() -> Self {
64 Self::default()
65 }
66
67 #[must_use]
69 pub fn on_rails(&self) -> bool {
70 self.state.lock().on_rails
71 }
72
73 pub fn set_on_rails(&self, value: bool) {
75 self.state.lock().on_rails = value;
76 }
77
78 fn flipped(&self) -> bool {
79 self.state.lock().flipped
80 }
81
82 pub fn set_flipped(&self, flipped: bool) {
84 self.state.lock().flipped = flipped;
85 }
86
87 pub fn toggle_flipped(&self) {
89 let mut state = self.state.lock();
90 state.flipped = !state.flipped;
91 }
92}
93
94pub trait AbstractMinecartEventSource {
96 fn as_abstract_minecart_event_source(&self) -> &dyn AbstractMinecart;
98}
99
100impl<T: AbstractMinecart> AbstractMinecartEventSource for T {
101 fn as_abstract_minecart_event_source(&self) -> &dyn AbstractMinecart {
102 self
103 }
104}
105
106pub trait AbstractMinecart: VehicleEntity + AbstractMinecartEventSource {
108 fn minecart_base(&self) -> &AbstractMinecartBase;
110
111 fn custom_display_block(&self) -> Option<BlockStateId>;
113
114 fn set_custom_display_block(&self, value: Option<BlockStateId>);
116
117 fn default_display_offset(&self) -> i32 {
119 6
120 }
121
122 fn max_speed(&self, world: &World) -> f64 {
124 MinecartBehavior::max_speed(self.as_abstract_minecart_event_source(), world)
125 }
126
127 fn minecart_default_gravity(&self) -> f64 {
129 if self.is_in_water() {
130 GRAVITY_IN_WATER
131 } else {
132 DEFAULT_GRAVITY
133 }
134 }
135
136 fn minecart_air_drag(&self) -> f64 {
138 f64::from(0.95_f32)
139 }
140
141 fn minecart_is_pickable(&self) -> bool {
143 !self.is_removed()
144 }
145
146 fn minecart_is_pushable(&self) -> bool {
148 true
149 }
150
151 fn minecart_movement_emission(&self) -> EntityMovementEmission {
153 EntityMovementEmission::Events
154 }
155
156 fn minecart_blocks_building(&self) -> bool {
158 true
159 }
160
161 fn minecart_can_collide_with(&self, other: &dyn Entity) -> bool {
163 (other.can_be_collided_with(Some(self.as_entity_event_source())) || other.is_pushable())
164 && !self.is_passenger_of_same_vehicle(other)
165 }
166
167 fn display_offset(&self) -> i32;
169
170 fn set_display_offset(&self, value: i32);
172
173 fn display_block_state(&self) -> BlockStateId {
175 self.custom_display_block()
176 .unwrap_or_else(|| self.default_display_block_state())
177 }
178
179 fn default_display_block_state(&self) -> BlockStateId {
181 vanilla_blocks::AIR.default_state()
182 }
183
184 fn activate_minecart(&self, _world: &World, _pos: BlockPos, _powered: bool) {}
186
187 fn is_rideable(&self) -> bool {
189 false
190 }
191
192 fn is_flipped(&self) -> bool {
194 self.minecart_base().flipped()
195 }
196
197 fn is_redstone_conductor(&self, world: &World, pos: BlockPos) -> bool {
199 let state = world.get_block_state(pos);
200 world.is_redstone_conductor(state, pos)
201 }
202
203 fn is_furnace(&self) -> bool {
205 false
206 }
207
208 fn current_block_pos_or_rail_below(&self, world: &World) -> BlockPos {
210 let pos = self.position();
211 let (xt, yt, zt) = (
212 pos.x.floor() as i32,
213 pos.y.floor() as i32,
214 pos.z.floor() as i32,
215 );
216 let below = BlockPos::new(xt, yt - 1, zt);
217 if world
218 .get_block_state(below)
219 .get_block()
220 .has_tag(&BlockTag::RAILS)
221 {
222 below
223 } else {
224 BlockPos::new(xt, yt, zt)
225 }
226 }
227
228 fn minecart_block_speed_factor(&self) -> f32 {
230 let Some(world) = self.level() else {
231 return 1.0;
232 };
233
234 let state = world.get_block_state(self.block_position());
235 if state.get_block().has_tag(&BlockTag::RAILS) {
236 1.0
237 } else {
238 self.default_block_speed_factor()
239 }
240 }
241
242 fn minecart_apply_effects_from_blocks(&self) {
244 let position = self.position();
245 self.apply_effects_from_blocks_between(position, position);
246 self.base().clear_movement_this_tick();
247 }
248
249 fn minecart_move_entity(
251 self: Arc<Self>,
252 mover_type: MoverType,
253 delta: DVec3,
254 ) -> Option<MoveResult> {
255 let result = Arc::clone(&self).default_move_entity(mover_type, delta);
256 self.minecart_apply_effects_from_blocks();
257 result
258 }
259
260 fn tick_minecart(&self, entity: &SharedEntity) {
262 let Some(world) = self.level() else {
263 return;
264 };
265
266 if self.hurt_time() > 0 {
267 self.set_hurt_time(self.hurt_time() - 1);
268 }
269 if self.damage() > 0.0 {
270 self.set_damage(self.damage() - 1.0);
271 }
272
273 self.check_below_world();
274 self.base().compute_known_speed();
275 self.handle_portal();
276 MinecartBehavior::tick(self.as_abstract_minecart_event_source(), entity, &world);
277 self.update_fluid_interaction();
278
279 if self.is_in_lava() {
280 self.lava_ignite();
281 self.lava_hurt();
282 self.set_fall_distance(self.fall_distance() * LAVA_FALL_DISTANCE_FACTOR);
283 }
284
285 self.set_first_tick(false);
286 }
287
288 fn apply_natural_slowdown(&self, movement: DVec3) -> DVec3 {
290 let slowdown = MinecartBehavior::slowdown_factor(self.as_abstract_minecart_event_source());
291 let mut result = movement * DVec3::new(slowdown, 0.0, slowdown);
292 if self.is_in_water() {
293 result *= f64::from(WATER_SLOWDOWN_FACTOR);
294 }
295 result
296 }
297
298 fn come_off_track(&self, entity: &SharedEntity, world: &World) {
300 let max_speed = self.max_speed(world);
301 let velocity = self.velocity();
302 let mut velocity = DVec3::new(
303 velocity.x.clamp(-max_speed, max_speed),
304 velocity.y,
305 velocity.z.clamp(-max_speed, max_speed),
306 );
307 if self.on_ground() {
308 velocity *= OFF_TRACK_GROUND_SLOWDOWN_FACTOR;
309 }
310 self.set_velocity(velocity);
311 if Arc::clone(entity)
312 .move_entity(MoverType::SelfMovement, self.velocity())
313 .is_none()
314 {
315 return;
316 }
317
318 if !self.on_ground() {
319 self.set_velocity(self.velocity() * self.get_air_drag());
320 }
321 }
322
323 #[expect(
324 clippy::neg_cmp_op_on_partial_ord,
325 reason = "negated comparisons preserve Vanilla's NaN branch behavior"
326 )]
327 fn minecart_push_entity(&self, other: &dyn Entity) {
329 if other.no_physics() || self.no_physics() || self.has_passenger(other) {
330 return;
331 }
332
333 let mut xa = other.position().x - self.position().x;
334 let mut za = other.position().z - self.position().z;
335 let dd = xa * xa + za * za;
336 if !(dd >= f64::from(MIN_PUSH_DISTANCE_SQUARED)) {
337 return;
338 }
339
340 let dd = dd.sqrt();
341 xa /= dd;
342 za /= dd;
343 let pow = (1.0 / dd).min(1.0);
344 xa *= pow;
345 za *= pow;
346 xa *= f64::from(PUSH_FORCE);
347 za *= f64::from(PUSH_FORCE);
348 xa *= COLLISION_PUSH_SCALE;
349 za *= COLLISION_PUSH_SCALE;
350
351 if let Some(other_minecart) = other.as_abstract_minecart() {
352 self.push_other_minecart(other_minecart, xa, za);
353 } else {
354 self.push_impulse(DVec3::new(-xa, 0.0, -za));
355 other.push_impulse(DVec3::new(
356 xa / NON_MINECART_PUSH_DIVISOR,
357 0.0,
358 za / NON_MINECART_PUSH_DIVISOR,
359 ));
360 }
361 }
362
363 #[expect(
364 clippy::manual_midpoint,
365 reason = "midpoint would change overflow vs vanilla"
366 )]
367 fn push_other_minecart(&self, other_minecart: &dyn AbstractMinecart, xa: f64, za: f64) {
370 let xo = other_minecart.position().x - self.position().x;
371 let zo = other_minecart.position().z - self.position().z;
372
373 let dir = DVec3::new(xo, 0.0, zo).normalize_or_zero();
374 let yaw = f64::from(self.rotation().0.to_radians());
375 let facing = DVec3::new(f64::from(trig::cos(yaw)), 0.0, f64::from(trig::sin(yaw)))
376 .normalize_or_zero();
377 let dot = dir.dot(facing).abs();
378
379 if dot < f64::from(MIN_COLLISION_ALIGNMENT) {
380 return;
381 }
382
383 let movement = self.velocity();
384 let other_movement = other_minecart.velocity();
385 let normal_scale = DVec3::new(
386 COLLISION_VELOCITY_RETENTION,
387 1.0,
388 COLLISION_VELOCITY_RETENTION,
389 );
390 let furnace_scale = DVec3::new(
391 FURNACE_COLLISION_VELOCITY_RETENTION,
392 1.0,
393 FURNACE_COLLISION_VELOCITY_RETENTION,
394 );
395
396 if other_minecart.is_furnace() && !self.is_furnace() {
397 self.set_velocity(movement * normal_scale);
398 self.push_impulse(DVec3::new(
399 other_movement.x - xa,
400 0.0,
401 other_movement.z - za,
402 ));
403 other_minecart.set_velocity(other_movement * furnace_scale);
404 } else if !other_minecart.is_furnace() && self.is_furnace() {
405 other_minecart.set_velocity(other_movement * normal_scale);
406 other_minecart.push_impulse(DVec3::new(movement.x + xa, 0.0, movement.z + za));
407 self.set_velocity(movement * furnace_scale);
408 } else {
409 let xdd = (other_movement.x + movement.x) / 2.0;
410 let zdd = (other_movement.z + movement.z) / 2.0;
411 self.set_velocity(movement * normal_scale);
412 self.push_impulse(DVec3::new(xdd - xa, 0.0, zdd - za));
413 other_minecart.set_velocity(other_movement * normal_scale);
414 other_minecart.push_impulse(DVec3::new(xdd + xa, 0.0, zdd + za));
415 }
416 }
417
418 fn minecart_passenger_attachment_point(&self, passenger: &dyn Entity) -> DVec3 {
422 let passenger_type = passenger.entity_type();
423
424 if passenger_type == &vanilla_entities::VILLAGER
425 || passenger_type == &vanilla_entities::WANDERING_TRADER
426 {
427 LOWERED_PASSENGER_ATTACHMENT
428 } else {
429 self.default_passenger_attachment_point(passenger)
430 }
431 }
432
433 fn save_abstract_minecart_data(&self, nbt: &mut NbtCompound) {
435 if let Some(display_state) = self.custom_display_block() {
436 nbt.insert(
437 "DisplayState",
438 NbtTag::Compound(block_state_nbt::save(display_state)),
439 );
440 }
441 if self.display_offset() != self.default_display_offset() {
442 nbt.insert("DisplayOffset", self.display_offset());
443 }
444 nbt.insert("FlippedRotation", i8::from(self.minecart_base().flipped()));
445 nbt.insert("HasTicked", i8::from(self.is_first_tick()));
446 }
447
448 fn load_abstract_minecart_data(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
450 self.set_custom_display_block(nbt.compound("DisplayState").and_then(block_state_nbt::load));
451 self.set_display_offset(
452 nbt.get("DisplayOffset")
453 .and_then(|tag| tag.int_value())
454 .unwrap_or_else(|| self.default_display_offset()),
455 );
456
457 let flipped = nbt
458 .get("FlippedRotation")
459 .and_then(|tag| tag.byte_value())
460 .is_some_and(|value| value != 0);
461 self.minecart_base().set_flipped(flipped);
462 self.set_first_tick(
463 nbt.get("HasTicked")
464 .and_then(|tag| tag.byte_value())
465 .is_some_and(|value| value != 0),
466 );
467 }
468}
469
470#[must_use]
472pub const fn rail_exits(shape: RailShape) -> ((i32, i32, i32), (i32, i32, i32)) {
473 const fn below((x, y, z): (i32, i32, i32)) -> (i32, i32, i32) {
474 (x, y - 1, z)
475 }
476
477 let north = Direction::North.offset();
478 let south = Direction::South.offset();
479 let west = Direction::West.offset();
480 let east = Direction::East.offset();
481 match shape {
482 RailShape::NorthSouth => (north, south),
483 RailShape::EastWest => (west, east),
484 RailShape::AscendingEast => (below(west), east),
485 RailShape::AscendingWest => (west, below(east)),
486 RailShape::AscendingNorth => (north, below(south)),
487 RailShape::AscendingSouth => (below(north), south),
488 RailShape::SouthEast => (south, east),
489 RailShape::SouthWest => (south, west),
490 RailShape::NorthWest => (north, west),
491 RailShape::NorthEast => (north, east),
492 }
493}
494
495pub(crate) fn minecart_relative_portal_position(
497 minecart: &dyn Entity,
498 axis: Axis,
499 portal_area: FoundRectangle,
500) -> DVec3 {
501 reset_forward_direction_of_relative_portal_position(PortalShape::get_relative_position(
502 portal_area,
503 axis,
504 minecart.position(),
505 minecart.dimensions_for_pose(minecart.pose()),
506 ))
507}
508
509#[derive(Debug)]
511pub(crate) enum CreateMinecartError {
512 Unsupported,
514 Spawn(EntitySpawnError),
516}
517
518pub(crate) fn create_minecart(
520 world: &Arc<World>,
521 position: DVec3,
522 entity_type: EntityTypeRef,
523 item_stack: &ItemStack,
524 player: Option<&Player>,
525) -> Result<SharedEntity, CreateMinecartError> {
526 let cart =
527 create_entity_instance(world, entity_type, position).map_err(|error| match error {
528 EntitySpawnError::MissingFactory => CreateMinecartError::Unsupported,
529 error => CreateMinecartError::Spawn(error),
530 })?;
531
532 if cart.as_abstract_minecart().is_none() {
533 return Err(CreateMinecartError::Unsupported);
534 }
535
536 cart.base().set_position_local(position);
537 cart.set_old_position_to_current();
538
539 apply_item_stack_components(&cart, item_stack, player.is_some_and(Player::is_operator))
540 .map_err(CreateMinecartError::Spawn)?;
541
542 Ok(cart)
543}
544
545#[cfg(test)]
546#[path = "tests/abstract_minecart.rs"]
547mod tests;