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