1use std::sync::Arc;
2
3use glam::DVec3;
4use glam::Vec3Swizzles;
5use steel_math::{DEGREE_180, DEGREE_360, RAD_TO_DEG_F64, trig::atan2, wrap_degrees};
6use steel_registry::blocks::block_state_ext::BlockStateExt;
7use steel_registry::blocks::properties::{
8 BlockStateProperties, BoolProperty, EnumProperty, RailShape,
9};
10use steel_registry::vanilla_block_tags::BlockTag;
11use steel_registry::vanilla_blocks;
12use steel_registry::vanilla_entities;
13use steel_utils::{BlockPos, Direction};
14
15use super::abstract_minecart::{AbstractMinecart, rail_exits};
16use crate::behavior::blocks::BaseRailBlock;
17use crate::entity::SharedEntity;
18use crate::physics::MoverType;
19use crate::world::World;
20
21const RAIL_SHAPE: &EnumProperty<RailShape> = &BlockStateProperties::RAIL_SHAPE;
22const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
23
24const RAIL_HEIGHT_OFFSET: f64 = 0.0625;
25const SLIDE_SPEED: f64 = 0.007_812_5;
26const WATER_SLIDE_SPEED: f64 = SLIDE_SPEED * 0.2;
27const MAX_SPEED_IN_WATER: f64 = 0.2;
28const MAX_SPEED_ON_LAND: f64 = 0.4;
29const ABSOLUTE_MAX_SPEED: f64 = 0.4;
30const MAX_RAIL_ALIGNMENT_VELOCITY: f64 = 2.0;
31const POWERED_RAIL_ACCEL: f64 = 0.06;
32const POWERED_RAIL_KICKSTART: f64 = 0.02;
33const RIDDEN_SLOWDOWN_FACTOR: f64 = 0.997;
34const EMPTY_SLOWDOWN_FACTOR: f64 = 0.96;
35const OCCUPIED_MOVEMENT_SCALE: f64 = 0.75;
36const PLAYER_NUDGE_ACCELERATION: f64 = 0.001;
37const PLAYER_NUDGE_MAX_SPEED_SQUARED: f64 = 0.01;
38const MINECART_RIDABLE_THRESHOLD: f64 = 0.01;
39const MIN_ROTATION_DISTANCE_SQUARED: f64 = 0.001;
40const ENTITY_INTERACTION_INFLATION: f64 = 0.2_f32 as f64;
41const UNPOWERED_RAIL_STOP_THRESHOLD: f64 = 0.03;
42const UNPOWERED_RAIL_SLOWDOWN_FACTOR: f64 = 0.5;
43const POWERED_RAIL_ACCELERATION_THRESHOLD: f64 = 0.01;
44const SLOPE_SPEED_FACTOR: f64 = 0.05;
45const ROTATION_FLIP_THRESHOLD_DEGREES: f32 = 170.0;
46const RAIL_EXIT_OFFSET_SCALE: f64 = 0.5;
47const CORNER_RAIL_PROJECTION_SCALE: f64 = 2.0;
48const SLOPE_HEIGHT_SCALE: f64 = 2.0;
49
50#[derive(Debug, Default)]
52pub struct MinecartBehavior;
53
54impl MinecartBehavior {
55 pub(crate) fn tick(minecart: &dyn AbstractMinecart, entity: &SharedEntity, world: &World) {
56 minecart.apply_gravity();
57 let pos = minecart.current_block_pos_or_rail_below(world);
58 let block_state = world.get_block_state(pos);
59 let on_rails = BaseRailBlock::is_rail_state(block_state);
60 minecart.minecart_base().set_on_rails(on_rails);
61
62 if on_rails {
63 Self::move_along_track(minecart, entity, world);
64 if block_state.get_block() == &vanilla_blocks::ACTIVATOR_RAIL {
65 minecart.activate_minecart(world, pos, block_state.get_value(POWERED));
66 }
67 } else {
68 minecart.come_off_track(entity, world);
69 }
70
71 minecart.apply_effects_from_blocks();
72 Self::update_rotation_from_movement(minecart);
73 Self::push_and_pickup_entities(minecart);
74 }
75
76 pub(crate) fn motion_direction(minecart: &dyn AbstractMinecart) -> Direction {
77 let direction = minecart.direction_yaw();
78
79 if minecart.is_flipped() {
80 direction.opposite().rotate_y_clockwise()
81 } else {
82 direction.rotate_y_clockwise()
83 }
84 }
85
86 pub(crate) fn known_movement(known_movement: DVec3) -> DVec3 {
88 if known_movement.x.is_nan() || known_movement.y.is_nan() || known_movement.z.is_nan() {
89 return DVec3::ZERO;
90 }
91
92 DVec3::new(
93 known_movement
94 .x
95 .clamp(-ABSOLUTE_MAX_SPEED, ABSOLUTE_MAX_SPEED),
96 known_movement.y,
97 known_movement
98 .z
99 .clamp(-ABSOLUTE_MAX_SPEED, ABSOLUTE_MAX_SPEED),
100 )
101 }
102
103 pub(crate) fn max_speed(minecart: &dyn AbstractMinecart, _world: &World) -> f64 {
104 if minecart.is_in_water() {
105 MAX_SPEED_IN_WATER
106 } else {
107 MAX_SPEED_ON_LAND
108 }
109 }
110
111 pub(crate) fn slowdown_factor(minecart: &dyn AbstractMinecart) -> f64 {
112 if minecart.is_vehicle() {
113 RIDDEN_SLOWDOWN_FACTOR
114 } else {
115 EMPTY_SLOWDOWN_FACTOR
116 }
117 }
118
119 #[expect(
120 clippy::too_many_lines,
121 reason = "keeping it together makes it easier to audit"
122 )]
123 fn move_along_track(minecart: &dyn AbstractMinecart, entity: &SharedEntity, world: &World) {
124 let pos = minecart.current_block_pos_or_rail_below(world);
125 let state = world.get_block_state(pos);
126 minecart.reset_fall_distance();
127
128 let position = minecart.position();
129 let old_projected = Self::get_pos(world, position.x, position.y, position.z, None);
130 let mut y = f64::from(pos.y());
131
132 let block = state.get_block();
133 let (power_track, mut halt_track) = if block == &vanilla_blocks::POWERED_RAIL {
134 let powered = state.get_value(POWERED);
135 (powered, !powered)
136 } else {
137 (false, false)
138 };
139
140 let slide_speed = if minecart.is_in_water() {
141 WATER_SLIDE_SPEED
142 } else {
143 SLIDE_SPEED
144 };
145 let shape = state.get_value(RAIL_SHAPE);
146 let mut velocity = minecart.velocity();
147 match shape {
148 RailShape::AscendingEast => velocity.x -= slide_speed,
149 RailShape::AscendingWest => velocity.x += slide_speed,
150 RailShape::AscendingNorth => velocity.z += slide_speed,
151 RailShape::AscendingSouth => velocity.z -= slide_speed,
152 _ => {}
153 }
154 if shape.is_slope() {
155 y += 1.0;
156 }
157 minecart.set_velocity(velocity);
158
159 let velocity = minecart.velocity();
160 let ((ex0, ey0, ez0), (ex1, ey1, ez1)) = rail_exits(shape);
161 let (mut xd, mut zd) = (f64::from(ex1 - ex0), f64::from(ez1 - ez0));
162 let length = (xd * xd + zd * zd).sqrt();
163 if velocity.x * xd + velocity.z * zd < 0.0 {
164 xd = -xd;
165 zd = -zd;
166 }
167 let pow = velocity.xz().length().min(MAX_RAIL_ALIGNMENT_VELOCITY);
168 minecart.set_velocity(DVec3::new(pow * xd / length, velocity.y, pow * zd / length));
169
170 if let Some(passenger) = minecart.first_passenger()
171 && let Some(player) = passenger.as_player()
172 {
173 let move_intent = player.last_client_move_intent();
174
175 if move_intent.length_squared() > 0.0 {
176 let rider_movement = move_intent.normalize();
177 let velocity = minecart.velocity();
178 let own_distance_squared = velocity.xz().length_squared();
179
180 if rider_movement.length_squared() > 0.0
181 && own_distance_squared < PLAYER_NUDGE_MAX_SPEED_SQUARED
182 {
183 minecart.set_velocity(
184 velocity
185 + DVec3::new(
186 move_intent.x * PLAYER_NUDGE_ACCELERATION,
187 0.0,
188 move_intent.z * PLAYER_NUDGE_ACCELERATION,
189 ),
190 );
191 halt_track = false;
192 }
193 }
194 }
195
196 if halt_track {
197 let velocity = minecart.velocity();
198 let speed_length = velocity.xz().length();
199 let velocity = if speed_length < UNPOWERED_RAIL_STOP_THRESHOLD {
200 DVec3::ZERO
201 } else {
202 velocity
203 * DVec3::new(
204 UNPOWERED_RAIL_SLOWDOWN_FACTOR,
205 0.0,
206 UNPOWERED_RAIL_SLOWDOWN_FACTOR,
207 )
208 };
209 minecart.set_velocity(velocity);
210 }
211
212 if let Some(projected) = Self::get_pos(
213 world,
214 position.x,
215 position.y,
216 position.z,
217 Some((pos, shape)),
218 ) && !Self::set_position(
219 minecart,
220 DVec3::new(projected.x, y, projected.z),
221 "project onto rail",
222 ) {
223 return;
224 }
225
226 let scale = if minecart.is_vehicle() {
227 OCCUPIED_MOVEMENT_SCALE
228 } else {
229 1.0
230 };
231 let max_speed = minecart.max_speed(world);
232 let velocity = minecart.velocity();
233 let clamped = DVec3::new(
234 (scale * velocity.x).clamp(-max_speed, max_speed),
235 0.0,
236 (scale * velocity.z).clamp(-max_speed, max_speed),
237 );
238 if Arc::clone(entity)
239 .move_entity(MoverType::SelfMovement, clamped)
240 .is_none()
241 {
242 return;
243 }
244
245 let position = minecart.position();
246 let (floor_x, floor_z) = (position.x.floor() as i32, position.z.floor() as i32);
247 let exit_y = if ey0 != 0 && floor_x - pos.x() == ex0 && floor_z - pos.z() == ez0 {
248 Some(ey0)
249 } else if ey1 != 0 && floor_x - pos.x() == ex1 && floor_z - pos.z() == ez1 {
250 Some(ey1)
251 } else {
252 None
253 };
254
255 if let Some(exit_y) = exit_y {
256 let position = minecart.position();
257 let raised = DVec3::new(position.x, position.y + f64::from(exit_y), position.z);
258 if !Self::set_position(minecart, raised, "raise to rail exit height") {
259 return;
260 }
261 }
262
263 minecart.set_velocity(minecart.apply_natural_slowdown(minecart.velocity()));
264
265 let position = minecart.position();
266 if let (Some(new_projected), Some(old_projected)) = (
267 Self::get_pos(world, position.x, position.y, position.z, None),
268 old_projected,
269 ) {
270 let slope_speed_adjustment = (old_projected.y - new_projected.y) * SLOPE_SPEED_FACTOR;
271 let velocity = minecart.velocity();
272 let horizontal = velocity.xz().length();
273 if horizontal > 0.0 {
274 let factor = (horizontal + slope_speed_adjustment) / horizontal;
275 minecart.set_velocity(DVec3::new(
276 velocity.x * factor,
277 velocity.y,
278 velocity.z * factor,
279 ));
280 }
281 let position = minecart.position();
282 let corrected = DVec3::new(position.x, new_projected.y, position.z);
283 if !Self::set_position(minecart, corrected, "correct rail height") {
284 return;
285 }
286 }
287
288 let position = minecart.position();
289 let (xn, zn) = (position.x.floor() as i32, position.z.floor() as i32);
290 if xn != pos.x() || zn != pos.z() {
291 let velocity = minecart.velocity();
292 let horizontal = velocity.xz().length();
293 minecart.set_velocity(DVec3::new(
294 horizontal * f64::from(xn - pos.x()),
295 velocity.y,
296 horizontal * f64::from(zn - pos.z()),
297 ));
298 }
299
300 if power_track {
301 let velocity = minecart.velocity();
302 let speed_length = velocity.xz().length();
303 if speed_length > POWERED_RAIL_ACCELERATION_THRESHOLD {
304 minecart.set_velocity(
305 velocity
306 + DVec3::new(
307 velocity.x / speed_length * POWERED_RAIL_ACCEL,
308 0.0,
309 velocity.z / speed_length * POWERED_RAIL_ACCEL,
310 ),
311 );
312 } else {
313 let (mut dx, mut dz) = (velocity.x, velocity.z);
314 match shape {
315 RailShape::EastWest => {
316 if minecart.is_redstone_conductor(world, pos.relative(Direction::West)) {
317 dx = POWERED_RAIL_KICKSTART;
318 } else if minecart
319 .is_redstone_conductor(world, pos.relative(Direction::East))
320 {
321 dx = -POWERED_RAIL_KICKSTART;
322 }
323 }
324 RailShape::NorthSouth => {
325 if minecart.is_redstone_conductor(world, pos.relative(Direction::North)) {
326 dz = POWERED_RAIL_KICKSTART;
327 } else if minecart
328 .is_redstone_conductor(world, pos.relative(Direction::South))
329 {
330 dz = -POWERED_RAIL_KICKSTART;
331 }
332 }
333 _ => return,
334 }
335 minecart.set_velocity(DVec3::new(dx, velocity.y, dz));
336 }
337 }
338 }
339
340 fn set_position(minecart: &dyn AbstractMinecart, position: DVec3, step: &str) -> bool {
343 let Err(error) = minecart.try_set_position(position) else {
344 return true;
345 };
346 log::debug!("failed to {step} for minecart {}: {error}", minecart.id());
347 false
348 }
349
350 fn push_and_pickup_entities(minecart: &dyn AbstractMinecart) -> bool {
352 let Some(world) = minecart.level() else {
353 return false;
354 };
355 let Some(minecart_shared) = world.get_entity_by_id(minecart.id()) else {
356 return false;
357 };
358
359 let hitbox = minecart.bounding_box().inflate_xyz(
360 ENTITY_INTERACTION_INFLATION,
361 0.0,
362 ENTITY_INTERACTION_INFLATION,
363 );
364
365 if minecart.is_rideable()
366 && minecart.velocity().xz().length_squared() >= MINECART_RIDABLE_THRESHOLD
367 {
368 let entities = world.get_pushable_entities(minecart.as_entity_event_source(), &hitbox);
369
370 for entity in entities {
371 let is_player = entity.entity_type() == &vanilla_entities::PLAYER;
372 let is_iron_golem = entity.entity_type() == &vanilla_entities::IRON_GOLEM;
373 let is_minecart = entity.entity_type().is_abstract_minecart;
374
375 if !is_player
376 && !is_iron_golem
377 && !is_minecart
378 && !minecart.is_vehicle()
379 && !entity.is_passenger()
380 {
381 entity.start_riding(&minecart_shared);
382 } else {
383 entity.push_entity(minecart.as_entity_event_source());
384 }
385 }
386 } else {
387 let entities =
388 world.get_entities_in_aabb_matching(&hitbox, |entity| entity.id() != minecart.id());
389
390 for entity in entities {
391 if !minecart.has_passenger(entity.as_ref())
392 && entity.is_pushable()
393 && entity.entity_type().is_abstract_minecart
394 {
395 entity.push_entity(minecart.as_entity_event_source());
396 }
397 }
398 }
399
400 false
401 }
402
403 fn update_rotation_from_movement(minecart: &dyn AbstractMinecart) {
404 minecart.set_rotation((minecart.rotation().0, 0.0));
405 let (old_position, position) = (minecart.old_position(), minecart.position());
406 let (x_diff, z_diff) = (old_position.x - position.x, old_position.z - position.z);
407 if x_diff * x_diff + z_diff * z_diff > MIN_ROTATION_DISTANCE_SQUARED {
408 let mut yaw = (atan2(z_diff, x_diff) * RAD_TO_DEG_F64) as f32;
409 if minecart.is_flipped() {
410 yaw += DEGREE_180;
411 }
412 minecart.set_rotation((yaw, minecart.rotation().1));
413 }
414
415 let rot_diff = wrap_degrees(minecart.rotation().0 - minecart.base().old_rotation().0);
416 if rot_diff < -ROTATION_FLIP_THRESHOLD_DEGREES
417 || rot_diff >= ROTATION_FLIP_THRESHOLD_DEGREES
418 {
419 minecart.minecart_base().toggle_flipped();
420 minecart.set_rotation((minecart.rotation().0 + DEGREE_180, minecart.rotation().1));
421 }
422 minecart.set_rotation((
423 minecart.rotation().0 % DEGREE_360,
424 minecart.rotation().1 % DEGREE_360,
425 ));
426 }
427
428 #[must_use]
430 fn get_pos(
431 world: &World,
432 x: f64,
433 y: f64,
434 z: f64,
435 rail: Option<(BlockPos, RailShape)>,
436 ) -> Option<DVec3> {
437 let (pos, shape) = if let Some(rail) = rail {
438 rail
439 } else {
440 let (xt, mut yt, zt) = (x.floor() as i32, y.floor() as i32, z.floor() as i32);
441 if world
442 .get_block_state(BlockPos::new(xt, yt - 1, zt))
443 .get_block()
444 .has_tag(&BlockTag::RAILS)
445 {
446 yt -= 1;
447 }
448
449 let pos = BlockPos::new(xt, yt, zt);
450 let state = world.get_block_state(pos);
451 if !BaseRailBlock::is_rail_state(state) {
452 return None;
453 }
454
455 (pos, state.get_value(RAIL_SHAPE))
456 };
457
458 let (xt, yt, zt) = (pos.x(), pos.y(), pos.z());
459 let ((ex0, ey0, ez0), (ex1, ey1, ez1)) = rail_exits(shape);
460 let (x0, y0, z0) = (
461 f64::from(xt) + 0.5 + f64::from(ex0) * RAIL_EXIT_OFFSET_SCALE,
462 f64::from(yt) + RAIL_HEIGHT_OFFSET + f64::from(ey0) * RAIL_EXIT_OFFSET_SCALE,
463 f64::from(zt) + 0.5 + f64::from(ez0) * RAIL_EXIT_OFFSET_SCALE,
464 );
465 let (x1, y1, z1) = (
466 f64::from(xt) + 0.5 + f64::from(ex1) * RAIL_EXIT_OFFSET_SCALE,
467 f64::from(yt) + RAIL_HEIGHT_OFFSET + f64::from(ey1) * RAIL_EXIT_OFFSET_SCALE,
468 f64::from(zt) + 0.5 + f64::from(ez1) * RAIL_EXIT_OFFSET_SCALE,
469 );
470 let (dx, dz) = (x1 - x0, z1 - z0);
471 let dy = (y1 - y0) * SLOPE_HEIGHT_SCALE;
472
473 let progress = if dx == 0.0 {
474 z - f64::from(zt)
475 } else if dz == 0.0 {
476 x - f64::from(xt)
477 } else {
478 ((x - x0) * dx + (z - z0) * dz) * CORNER_RAIL_PROJECTION_SCALE
479 };
480
481 let mut projected = DVec3::new(x0 + dx * progress, y0 + dy * progress, z0 + dz * progress);
482 if dy < 0.0 {
483 projected.y += 1.0;
484 } else if dy > 0.0 {
485 projected.y += 0.5;
486 }
487 Some(projected)
488 }
489}
490
491#[cfg(test)]
492#[path = "tests/minecart_behavior.rs"]
493mod tests;