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