1use crate::entity::{Entity, PendingWorldChangeToken};
8use crate::world::World;
9use glam::DVec3;
10use smallvec::SmallVec;
11use std::sync::Arc;
12use steel_protocol::packets::game::RelativeMovement;
13use steel_registry::game_rules::GameRuleRef;
14use steel_registry::vanilla_game_rules::{
15 PLAYERS_NETHER_PORTAL_CREATIVE_DELAY, PLAYERS_NETHER_PORTAL_DEFAULT_DELAY,
16};
17use steel_utils::BlockPos;
18
19pub(crate) mod end_gateway;
20pub(crate) mod end_portal;
21pub(crate) mod nether_portal;
22pub mod portal_shape;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum PortalKind {
31 Nether,
33 End,
35 EndGateway,
37}
38
39impl PortalKind {
40 #[must_use]
42 pub fn transition_time(self, world: &World, entity: &dyn Entity) -> i32 {
43 let player_invulnerable = entity
44 .as_player()
45 .map(|player| player.abilities.lock().invulnerable);
46 self.transition_time_for_player_state(world, player_invulnerable)
47 }
48
49 #[must_use]
51 pub fn transition_time_for_player_state(
52 self,
53 world: &World,
54 player_invulnerable: Option<bool>,
55 ) -> i32 {
56 match self {
57 Self::Nether => nether_portal_transition_time(world, player_invulnerable),
58 Self::End | Self::EndGateway => 0,
59 }
60 }
61}
62
63fn nether_portal_transition_time(world: &World, player_invulnerable: Option<bool>) -> i32 {
64 let Some(player_invulnerable) = player_invulnerable else {
65 return 0;
66 };
67
68 let rule = nether_portal_transition_rule(player_invulnerable);
69 let delay = portal_transition_game_rule(world, rule);
70 clamped_portal_transition_time(delay)
71}
72
73fn nether_portal_transition_rule(player_invulnerable: bool) -> GameRuleRef<i32> {
74 if player_invulnerable {
75 &PLAYERS_NETHER_PORTAL_CREATIVE_DELAY
76 } else {
77 &PLAYERS_NETHER_PORTAL_DEFAULT_DELAY
78 }
79}
80
81fn clamped_portal_transition_time(delay: i32) -> i32 {
82 delay.max(0)
83}
84
85fn portal_transition_game_rule(world: &World, rule: GameRuleRef<i32>) -> i32 {
86 world.get_game_rule(rule)
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum PortalProcessResult {
92 Waiting,
94 Ready,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct PortalProcessor {
105 portal: PortalKind,
106 entry_position: BlockPos,
107 portal_time: i32,
108 inside_portal_this_tick: bool,
109}
110
111impl PortalProcessor {
112 #[must_use]
114 pub const fn new(portal: PortalKind, entry_position: BlockPos) -> Self {
115 Self {
116 portal,
117 entry_position,
118 portal_time: 0,
119 inside_portal_this_tick: true,
120 }
121 }
122
123 #[must_use]
125 pub const fn portal(self) -> PortalKind {
126 self.portal
127 }
128
129 #[must_use]
131 pub const fn entry_position(self) -> BlockPos {
132 self.entry_position
133 }
134
135 #[must_use]
137 pub const fn portal_time(self) -> i32 {
138 self.portal_time
139 }
140
141 #[must_use]
143 pub const fn is_inside_portal_this_tick(self) -> bool {
144 self.inside_portal_this_tick
145 }
146
147 #[must_use]
149 pub fn is_same_portal(self, portal: PortalKind) -> bool {
150 self.portal == portal
151 }
152
153 pub const fn set_as_inside_portal(&mut self, entry_position: BlockPos) {
155 if !self.inside_portal_this_tick {
156 self.entry_position = entry_position;
157 self.inside_portal_this_tick = true;
158 }
159 }
160
161 pub fn process_portal_teleportation(
163 &mut self,
164 allowed_to_teleport: bool,
165 transition_time: i32,
166 ) -> PortalProcessResult {
167 if !self.inside_portal_this_tick {
168 self.decay_tick();
169 return PortalProcessResult::Waiting;
170 }
171
172 self.inside_portal_this_tick = false;
173 if !allowed_to_teleport {
174 return PortalProcessResult::Waiting;
175 }
176
177 let ready = self.portal_time >= transition_time;
178 self.portal_time += 1;
179 if ready {
180 PortalProcessResult::Ready
181 } else {
182 PortalProcessResult::Waiting
183 }
184 }
185
186 fn decay_tick(&mut self) {
187 self.portal_time = self.portal_time.saturating_sub(4).max(0);
188 }
189
190 #[must_use]
192 pub const fn has_expired(self) -> bool {
193 self.portal_time <= 0
194 }
195}
196
197#[derive(Clone)]
203pub struct TeleportTransition {
204 pub target_world: Arc<World>,
206 pub position: DVec3,
208 pub rotation: (f32, f32),
210 pub velocity: DVec3,
212 pub relatives: RelativeMovement,
214 pub portal_cooldown: i32,
216 pub as_passenger: bool,
218 pub post_transition: TeleportPostTransition,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct TeleportPostTransition {
225 actions: SmallVec<[TeleportPostAction; 2]>,
226}
227
228impl TeleportPostTransition {
229 #[must_use]
231 pub fn do_nothing() -> Self {
232 Self {
233 actions: SmallVec::new(),
234 }
235 }
236
237 #[must_use]
239 pub fn play_portal_sound() -> Self {
240 Self::single(TeleportPostAction::PlayPortalSound)
241 }
242
243 #[must_use]
245 pub fn place_portal_ticket(target: PortalTicketTarget) -> Self {
246 Self::single(TeleportPostAction::PlacePortalTicket(target))
247 }
248
249 #[must_use]
251 pub fn then(mut self, next: Self) -> Self {
252 self.actions.extend(next.actions);
253 self
254 }
255
256 #[must_use]
258 pub fn actions(&self) -> &[TeleportPostAction] {
259 self.actions.as_slice()
260 }
261
262 fn single(action: TeleportPostAction) -> Self {
263 let mut actions = SmallVec::new();
264 actions.push(action);
265 Self { actions }
266 }
267}
268
269impl Default for TeleportPostTransition {
270 fn default() -> Self {
271 Self::do_nothing()
272 }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub enum TeleportPostAction {
278 PlayPortalSound,
280 PlacePortalTicket(PortalTicketTarget),
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub enum PortalTicketTarget {
287 Destination,
289 Block(BlockPos),
291}
292
293impl TeleportTransition {
294 #[must_use]
296 pub fn with_position(&self, position: DVec3) -> Self {
297 Self {
298 target_world: self.target_world.clone(),
299 position,
300 rotation: self.rotation,
301 velocity: self.velocity,
302 relatives: self.relatives,
303 portal_cooldown: self.portal_cooldown,
304 as_passenger: self.as_passenger,
305 post_transition: self.post_transition.clone(),
306 }
307 }
308
309 #[must_use]
311 pub fn resolved_position(&self, current_position: DVec3) -> DVec3 {
312 resolve_position(self.position, self.relatives, current_position)
313 }
314
315 #[must_use]
317 pub fn resolved_rotation(&self, current_rotation: (f32, f32)) -> (f32, f32) {
318 resolve_rotation(self.rotation, self.relatives, current_rotation)
319 }
320
321 #[must_use]
323 pub fn resolved_velocity(
324 &self,
325 current_velocity: DVec3,
326 current_rotation: (f32, f32),
327 resolved_rotation: (f32, f32),
328 ) -> DVec3 {
329 resolve_velocity(
330 self.velocity,
331 self.relatives,
332 current_velocity,
333 current_rotation,
334 resolved_rotation,
335 )
336 }
337}
338
339fn resolve_position(
340 position: DVec3,
341 relatives: RelativeMovement,
342 current_position: DVec3,
343) -> DVec3 {
344 DVec3::new(
345 if relatives.is_x_relative() {
346 current_position.x + position.x
347 } else {
348 position.x
349 },
350 if relatives.is_y_relative() {
351 current_position.y + position.y
352 } else {
353 position.y
354 },
355 if relatives.is_z_relative() {
356 current_position.z + position.z
357 } else {
358 position.z
359 },
360 )
361}
362
363fn resolve_rotation(
364 rotation: (f32, f32),
365 relatives: RelativeMovement,
366 current_rotation: (f32, f32),
367) -> (f32, f32) {
368 let yaw = if relatives.is_y_rot_relative() {
369 current_rotation.0 + rotation.0
370 } else {
371 rotation.0
372 };
373 let pitch = if relatives.is_x_rot_relative() {
374 current_rotation.1 + rotation.1
375 } else {
376 rotation.1
377 };
378 (yaw, clamp_pitch(pitch))
379}
380
381const fn clamp_pitch(pitch: f32) -> f32 {
382 pitch.clamp(-90.0, 90.0)
383}
384
385fn resolve_velocity(
386 velocity: DVec3,
387 relatives: RelativeMovement,
388 current_velocity: DVec3,
389 current_rotation: (f32, f32),
390 resolved_rotation: (f32, f32),
391) -> DVec3 {
392 let current_velocity = if relatives.rotates_delta() {
393 let diff_yaw = current_rotation.0 - resolved_rotation.0;
394 let diff_pitch = current_rotation.1 - resolved_rotation.1;
395 rotate_y(
396 rotate_x(current_velocity, diff_pitch.to_radians()),
397 diff_yaw.to_radians(),
398 )
399 } else {
400 current_velocity
401 };
402
403 DVec3::new(
404 if relatives.is_delta_x_relative() {
405 current_velocity.x + velocity.x
406 } else {
407 velocity.x
408 },
409 if relatives.is_delta_y_relative() {
410 current_velocity.y + velocity.y
411 } else {
412 velocity.y
413 },
414 if relatives.is_delta_z_relative() {
415 current_velocity.z + velocity.z
416 } else {
417 velocity.z
418 },
419 )
420}
421
422fn rotate_x(vec: DVec3, radians: f32) -> DVec3 {
423 let cos = f64::from(radians.cos());
424 let sin = f64::from(radians.sin());
425 DVec3::new(vec.x, vec.y * cos + vec.z * sin, vec.z * cos - vec.y * sin)
426}
427
428fn rotate_y(vec: DVec3, radians: f32) -> DVec3 {
429 let cos = f64::from(radians.cos());
430 let sin = f64::from(radians.sin());
431 DVec3::new(vec.x * cos + vec.z * sin, vec.y, vec.z * cos - vec.x * sin)
432}
433
434pub enum WorldChangeRequest {
440 Computed(TeleportTransition),
442 WorldSpawn {
444 target_world: Arc<World>,
446 pending_token: PendingWorldChangeToken,
448 },
449 Portal {
451 portal: PortalKind,
453 source_world: Arc<World>,
455 portal_pos: BlockPos,
457 pending_token: PendingWorldChangeToken,
459 },
460}
461
462#[cfg(test)]
463mod tests {
464 use glam::DVec3;
465 use steel_protocol::packets::game::RelativeMovement;
466 use steel_registry::vanilla_game_rules::{
467 PLAYERS_NETHER_PORTAL_CREATIVE_DELAY, PLAYERS_NETHER_PORTAL_DEFAULT_DELAY,
468 };
469 use steel_utils::BlockPos;
470
471 use super::{
472 PortalKind, PortalProcessResult, PortalProcessor, PortalTicketTarget, TeleportPostAction,
473 TeleportPostTransition, clamped_portal_transition_time, nether_portal_transition_rule,
474 resolve_position, resolve_rotation, resolve_velocity,
475 };
476
477 #[test]
478 fn portal_processor_reaches_transition_after_vanilla_threshold() {
479 let mut processor = PortalProcessor::new(PortalKind::Nether, BlockPos::new(1, 64, 1));
480
481 assert_eq!(
482 processor.process_portal_teleportation(true, 2),
483 PortalProcessResult::Waiting
484 );
485 processor.set_as_inside_portal(BlockPos::new(1, 64, 1));
486 assert_eq!(
487 processor.process_portal_teleportation(true, 2),
488 PortalProcessResult::Waiting
489 );
490 processor.set_as_inside_portal(BlockPos::new(1, 64, 1));
491 assert_eq!(
492 processor.process_portal_teleportation(true, 2),
493 PortalProcessResult::Ready
494 );
495 assert_eq!(processor.portal_time(), 3);
496 }
497
498 #[test]
499 fn portal_processor_does_not_increment_when_teleport_is_disallowed() {
500 let mut processor = PortalProcessor::new(PortalKind::End, BlockPos::new(0, 80, 0));
501
502 assert_eq!(
503 processor.process_portal_teleportation(false, 0),
504 PortalProcessResult::Waiting
505 );
506
507 assert_eq!(processor.portal_time(), 0);
508 assert!(!processor.is_inside_portal_this_tick());
509 }
510
511 #[test]
512 fn portal_processor_decays_when_entity_leaves_portal() {
513 let mut processor = PortalProcessor::new(PortalKind::EndGateway, BlockPos::new(3, 70, 4));
514 for _ in 0..5 {
515 processor.set_as_inside_portal(BlockPos::new(3, 70, 4));
516 processor.process_portal_teleportation(true, 20);
517 }
518
519 assert_eq!(processor.portal_time(), 5);
520 processor.process_portal_teleportation(true, 20);
521 assert_eq!(processor.portal_time(), 1);
522 processor.process_portal_teleportation(true, 20);
523 assert_eq!(processor.portal_time(), 0);
524 assert!(processor.has_expired());
525 }
526
527 #[test]
528 fn portal_processor_updates_entry_position_only_after_tick_is_consumed() {
529 let mut processor = PortalProcessor::new(PortalKind::Nether, BlockPos::new(1, 64, 1));
530
531 processor.set_as_inside_portal(BlockPos::new(2, 64, 2));
532 assert_eq!(processor.entry_position(), BlockPos::new(1, 64, 1));
533
534 processor.process_portal_teleportation(true, 80);
535 processor.set_as_inside_portal(BlockPos::new(2, 64, 2));
536 assert_eq!(processor.entry_position(), BlockPos::new(2, 64, 2));
537 }
538
539 #[test]
540 fn nether_portal_transition_rule_matches_player_invulnerability() {
541 assert_eq!(
542 nether_portal_transition_rule(false).key(),
543 PLAYERS_NETHER_PORTAL_DEFAULT_DELAY.key()
544 );
545 assert_eq!(
546 nether_portal_transition_rule(true).key(),
547 PLAYERS_NETHER_PORTAL_CREATIVE_DELAY.key()
548 );
549 }
550
551 #[test]
552 fn portal_transition_time_is_clamped_non_negative() {
553 assert_eq!(clamped_portal_transition_time(-12), 0);
554 assert_eq!(clamped_portal_transition_time(0), 0);
555 assert_eq!(clamped_portal_transition_time(80), 80);
556 }
557
558 #[test]
559 fn relative_portal_transition_rotates_velocity_by_yaw_delta() {
560 let resolved_rotation =
561 resolve_rotation((90.0, 0.0), RelativeMovement::ROTATION, (0.0, 0.0));
562 let velocity = resolve_velocity(
563 DVec3::ZERO,
564 RelativeMovement::DELTA,
565 DVec3::new(1.0, 0.0, 0.0),
566 (0.0, 0.0),
567 resolved_rotation,
568 );
569
570 assert_eq!(resolved_rotation, (90.0, 0.0));
571 assert!((velocity - DVec3::new(0.0, 0.0, 1.0)).length_squared() < 1.0e-12);
572 }
573
574 #[test]
575 fn relative_position_transition_resolves_only_flagged_axes() {
576 assert_eq!(
577 resolve_position(
578 DVec3::new(1.0, 2.0, 3.0),
579 RelativeMovement::new(RelativeMovement::X | RelativeMovement::Z),
580 DVec3::new(10.0, 20.0, 30.0),
581 ),
582 DVec3::new(11.0, 2.0, 33.0)
583 );
584 }
585
586 #[test]
587 fn pitch_relative_transition_uses_absolute_yaw_and_relative_pitch() {
588 assert_eq!(
589 resolve_rotation(
590 (90.0, 0.0),
591 RelativeMovement::new(RelativeMovement::X_ROT),
592 (30.0, 15.0),
593 ),
594 (90.0, 15.0)
595 );
596 }
597
598 #[test]
599 fn resolved_rotation_clamps_pitch_like_vanilla() {
600 assert_eq!(
601 resolve_rotation((0.0, 30.0), RelativeMovement::ROTATION, (0.0, 80.0),),
602 (0.0, 90.0)
603 );
604 assert_eq!(
605 resolve_rotation((0.0, -120.0), RelativeMovement::NONE, (0.0, 0.0)),
606 (0.0, -90.0)
607 );
608 }
609
610 #[test]
611 fn absolute_transition_replaces_velocity_and_rotation() {
612 let resolved_rotation =
613 resolve_rotation((45.0, 10.0), RelativeMovement::NONE, (90.0, 20.0));
614
615 assert_eq!(resolved_rotation, (45.0, 10.0));
616 assert_eq!(
617 resolve_velocity(
618 DVec3::new(0.0, -0.1, 0.0),
619 RelativeMovement::NONE,
620 DVec3::new(1.0, 2.0, 3.0),
621 (90.0, 20.0),
622 resolved_rotation
623 ),
624 DVec3::new(0.0, -0.1, 0.0)
625 );
626 }
627
628 #[test]
629 fn post_transition_composition_preserves_vanilla_order() {
630 let transition = TeleportPostTransition::play_portal_sound().then(
631 TeleportPostTransition::place_portal_ticket(PortalTicketTarget::Block(BlockPos::new(
632 1, 64, 2,
633 ))),
634 );
635
636 assert_eq!(
637 transition.actions(),
638 &[
639 TeleportPostAction::PlayPortalSound,
640 TeleportPostAction::PlacePortalTicket(PortalTicketTarget::Block(BlockPos::new(
641 1, 64, 2,
642 ))),
643 ]
644 );
645 }
646}