Skip to main content

steel_core/command/builtins/
teleport.rs

1//! Vanilla entity teleport command.
2
3use std::{slice, sync::Arc};
4
5use glam::DVec3;
6use steel_protocol::packets::game::{AnimateAction, CAnimate, CSetCamera, RelativeMovement};
7use steel_registry::entity_data::EntityPose;
8use steel_utils::{BlockPos, Identifier, translations};
9use text_components::TextComponent;
10
11use super::super::{
12    brigadier::{CommandNodeBuilder, CommandSyntaxError},
13    execution::{
14        CommandSource, Coordinates, SteelArgumentType, SteelCommandContext, SteelCommandRuntime,
15        argument, literal,
16    },
17    registration::CommandRegistration,
18};
19use crate::{
20    entity::{Entity, EntityAnchor, LivingEntity as _, SharedEntity, change_entity_world},
21    portal::{TeleportPostTransition, TeleportTransition},
22    world::World,
23};
24
25pub(super) fn registration() -> CommandRegistration<CommandSource> {
26    CommandRegistration::new(Identifier::vanilla_static("teleport"), |_| command()).alias("tp")
27}
28
29fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
30    literal("teleport")
31        .then(
32            argument("location", SteelArgumentType::vec3(true))
33                .executes(teleport_source_to_position),
34        )
35        .then(
36            argument("destination", SteelArgumentType::entity())
37                .executes(teleport_source_to_entity),
38        )
39        .then(
40            argument("targets", SteelArgumentType::entities())
41                .then(target_position_branch())
42                .then(
43                    argument("destination", SteelArgumentType::entity())
44                        .executes(teleport_targets_to_entity),
45                ),
46        )
47}
48
49fn target_position_branch() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
50    argument("location", SteelArgumentType::vec3(true))
51        .executes(teleport_targets_to_position)
52        .then(
53            argument("rotation", SteelArgumentType::rotation())
54                .executes(teleport_targets_to_position_with_rotation),
55        )
56        .then(
57            literal("facing")
58                .then(
59                    literal("entity").then(
60                        argument("facingEntity", SteelArgumentType::entity())
61                            .executes(teleport_targets_facing_entity_feet)
62                            .then(
63                                argument("facingAnchor", SteelArgumentType::entity_anchor())
64                                    .executes(teleport_targets_facing_entity_anchor),
65                            ),
66                    ),
67                )
68                .then(
69                    argument("facingLocation", SteelArgumentType::vec3(true))
70                        .executes(teleport_targets_facing_position),
71                ),
72        )
73}
74
75fn teleport_source_to_position(
76    context: &SteelCommandContext<CommandSource>,
77) -> Result<i32, CommandSyntaxError> {
78    let target = source_entity(context)?;
79    let destination = required_coordinates(context, "location")?;
80    teleport_to_position(context, slice::from_ref(target), destination, None, None)
81}
82
83fn teleport_source_to_entity(
84    context: &SteelCommandContext<CommandSource>,
85) -> Result<i32, CommandSyntaxError> {
86    let target = source_entity(context)?;
87    let destination = context.entity("destination")?;
88    teleport_to_entity(context, slice::from_ref(target), &destination)
89}
90
91fn teleport_targets_to_position(
92    context: &SteelCommandContext<CommandSource>,
93) -> Result<i32, CommandSyntaxError> {
94    let targets = context.entities("targets")?;
95    let destination = required_coordinates(context, "location")?;
96    teleport_to_position(context, &targets, destination, None, None)
97}
98
99fn teleport_targets_to_position_with_rotation(
100    context: &SteelCommandContext<CommandSource>,
101) -> Result<i32, CommandSyntaxError> {
102    let targets = context.entities("targets")?;
103    let destination = required_coordinates(context, "location")?;
104    let rotation = required_coordinates(context, "rotation")?;
105    teleport_to_position(context, &targets, destination, Some(rotation), None)
106}
107
108fn teleport_targets_facing_entity_feet(
109    context: &SteelCommandContext<CommandSource>,
110) -> Result<i32, CommandSyntaxError> {
111    teleport_targets_facing_entity(context, EntityAnchor::Feet)
112}
113
114fn teleport_targets_facing_entity_anchor(
115    context: &SteelCommandContext<CommandSource>,
116) -> Result<i32, CommandSyntaxError> {
117    let Some(anchor) = context.entity_anchor("facingAnchor") else {
118        return Err(missing_argument("facingAnchor"));
119    };
120    teleport_targets_facing_entity(context, anchor)
121}
122
123fn teleport_targets_facing_entity(
124    context: &SteelCommandContext<CommandSource>,
125    anchor: EntityAnchor,
126) -> Result<i32, CommandSyntaxError> {
127    let targets = context.entities("targets")?;
128    let destination = required_coordinates(context, "location")?;
129    let facing_entity = context.entity("facingEntity")?;
130    teleport_to_position(
131        context,
132        &targets,
133        destination,
134        None,
135        Some(TeleportFacing::Entity {
136            target: facing_entity,
137            anchor,
138        }),
139    )
140}
141
142fn teleport_targets_facing_position(
143    context: &SteelCommandContext<CommandSource>,
144) -> Result<i32, CommandSyntaxError> {
145    let targets = context.entities("targets")?;
146    let destination = required_coordinates(context, "location")?;
147    let facing = required_coordinates(context, "facingLocation")?.position(context.source());
148    teleport_to_position(
149        context,
150        &targets,
151        destination,
152        None,
153        Some(TeleportFacing::Position(facing)),
154    )
155}
156
157fn teleport_targets_to_entity(
158    context: &SteelCommandContext<CommandSource>,
159) -> Result<i32, CommandSyntaxError> {
160    let targets = context.entities("targets")?;
161    let destination = context.entity("destination")?;
162    teleport_to_entity(context, &targets, &destination)
163}
164
165fn source_entity(
166    context: &SteelCommandContext<CommandSource>,
167) -> Result<&SharedEntity, CommandSyntaxError> {
168    context.source().entity().ok_or_else(|| {
169        CommandSyntaxError::dynamic(TextComponent::from(
170            &translations::PERMISSIONS_REQUIRES_ENTITY,
171        ))
172    })
173}
174
175fn required_coordinates(
176    context: &SteelCommandContext<CommandSource>,
177    name: &str,
178) -> Result<Coordinates, CommandSyntaxError> {
179    context
180        .coordinates(name)
181        .ok_or_else(|| missing_argument(name))
182}
183
184fn teleport_to_entity(
185    context: &SteelCommandContext<CommandSource>,
186    targets: &[SharedEntity],
187    destination: &SharedEntity,
188) -> Result<i32, CommandSyntaxError> {
189    let Some(target_world) = destination.level() else {
190        return Err(CommandSyntaxError::dynamic(
191            "Teleport destination is not in a live world",
192        ));
193    };
194    let position = destination.position();
195    ensure_spawnable_position(position)?;
196    let rotation = destination.rotation();
197    ensure_same_domain_targets(targets, &target_world)?;
198
199    for target in targets {
200        let transition = TeleportTransition {
201            target_world: Arc::clone(&target_world),
202            position,
203            rotation: wrap_rotation(rotation),
204            velocity: DVec3::ZERO,
205            relatives: RelativeMovement::NONE,
206            portal_cooldown: 0,
207            as_passenger: false,
208            post_transition: TeleportPostTransition::do_nothing(),
209        };
210        perform_teleport(context.source(), target, transition, None);
211    }
212
213    send_entity_success(context.source(), targets, destination.as_ref());
214    target_count(targets)
215}
216
217fn teleport_to_position(
218    context: &SteelCommandContext<CommandSource>,
219    targets: &[SharedEntity],
220    destination: Coordinates,
221    rotation: Option<Coordinates>,
222    facing: Option<TeleportFacing>,
223) -> Result<i32, CommandSyntaxError> {
224    let source = context.source();
225    let position = destination.position(source);
226    ensure_spawnable_position(position)?;
227    let resolved_rotation = rotation.map(|rotation| rotation.rotation(source));
228    ensure_same_domain_targets(targets, source.world())?;
229
230    for target in targets {
231        let same_world = target
232            .level()
233            .is_some_and(|world| Arc::ptr_eq(&world, source.world()));
234        let relatives = teleport_relatives(
235            (
236                destination.is_x_relative(),
237                destination.is_y_relative(),
238                destination.is_z_relative(),
239            ),
240            rotation.map(|rotation| (rotation.is_y_relative(), rotation.is_x_relative())),
241            same_world,
242        );
243        let target_rotation = target.rotation();
244        let desired_rotation = resolved_rotation.unwrap_or(target_rotation);
245        let transition = TeleportTransition {
246            target_world: Arc::clone(source.world()),
247            position: packet_position(position, target.position(), relatives),
248            rotation: packet_rotation(desired_rotation, target_rotation, relatives),
249            velocity: DVec3::ZERO,
250            relatives,
251            portal_cooldown: 0,
252            as_passenger: false,
253            post_transition: TeleportPostTransition::do_nothing(),
254        };
255        perform_teleport(source, target, transition, facing.as_ref());
256    }
257
258    send_position_success(source, targets, position);
259    target_count(targets)
260}
261
262enum TeleportFacing {
263    Position(DVec3),
264    Entity {
265        target: SharedEntity,
266        anchor: EntityAnchor,
267    },
268}
269
270fn perform_teleport(
271    source: &CommandSource,
272    target: &SharedEntity,
273    transition: TeleportTransition,
274    facing: Option<&TeleportFacing>,
275) {
276    if let Some(player) = target.as_player() {
277        if player.is_sleeping() {
278            let world = player.get_world();
279            world.broadcast_to_entity_trackers(
280                player.id(),
281                CAnimate::new(player.id(), AnimateAction::WakeUp),
282                None,
283            );
284            player.send_packet(CAnimate::new(player.id(), AnimateAction::WakeUp));
285            player.stop_sleeping();
286            player.set_pose(EntityPose::Standing);
287            // TODO: Complete bed occupancy and sleep aggregation updates with the bed system.
288        }
289        player.send_packet(CSetCamera {
290            camera_id: player.id(),
291        });
292    }
293    if change_entity_world(Arc::clone(target), &transition).is_none() {
294        return;
295    }
296
297    // Vanilla applies command post-effects through the original target reference.
298    if let Some(facing) = facing {
299        match facing {
300            TeleportFacing::Position(position) => target.look_at(source.anchor(), *position),
301            TeleportFacing::Entity {
302                target: facing_target,
303                anchor,
304            } => target.look_at_entity(source.anchor(), facing_target.as_ref(), *anchor),
305        }
306    }
307
308    if target
309        .as_living_entity()
310        .is_none_or(|living| !living.is_fall_flying())
311    {
312        let velocity = target.velocity();
313        target.set_velocity(DVec3::new(velocity.x, 0.0, velocity.z));
314        target.set_on_ground(true);
315    }
316    if let Some(pathfinder) = target.as_pathfinder_mob() {
317        pathfinder.mob_base().navigation().lock().stop();
318    }
319}
320
321fn ensure_spawnable_position(position: DVec3) -> Result<(), CommandSyntaxError> {
322    if World::is_in_spawnable_bounds(BlockPos::from(position)) {
323        return Ok(());
324    }
325    Err(CommandSyntaxError::dynamic(TextComponent::from(
326        &translations::COMMANDS_TELEPORT_INVALID_POSITION,
327    )))
328}
329
330fn ensure_same_domain_targets(
331    targets: &[SharedEntity],
332    target_world: &World,
333) -> Result<(), CommandSyntaxError> {
334    for target in targets {
335        let Some(source_world) = target.level() else {
336            continue;
337        };
338        ensure_same_domain(source_world.domain(), target_world.domain())?;
339    }
340    Ok(())
341}
342
343fn ensure_same_domain(source_domain: &str, target_domain: &str) -> Result<(), CommandSyntaxError> {
344    if source_domain == target_domain {
345        return Ok(());
346    }
347    Err(CommandSyntaxError::dynamic(
348        "Entities cannot be teleported across Steel domains",
349    ))
350}
351
352fn teleport_relatives(
353    position_relative: (bool, bool, bool),
354    rotation_relative: Option<(bool, bool)>,
355    same_world: bool,
356) -> RelativeMovement {
357    let mut flags = 0;
358    let (relative_x, relative_y, relative_z) = position_relative;
359    if relative_x {
360        flags |= RelativeMovement::DELTA_X;
361        if same_world {
362            flags |= RelativeMovement::X;
363        }
364    }
365    if relative_y {
366        flags |= RelativeMovement::DELTA_Y;
367        if same_world {
368            flags |= RelativeMovement::Y;
369        }
370    }
371    if relative_z {
372        flags |= RelativeMovement::DELTA_Z;
373        if same_world {
374            flags |= RelativeMovement::Z;
375        }
376    }
377
378    let (relative_yaw, relative_pitch) = rotation_relative.unwrap_or((true, true));
379    if relative_yaw {
380        flags |= RelativeMovement::Y_ROT;
381    }
382    if relative_pitch {
383        flags |= RelativeMovement::X_ROT;
384    }
385    RelativeMovement::new(flags)
386}
387
388fn packet_position(destination: DVec3, current: DVec3, relatives: RelativeMovement) -> DVec3 {
389    DVec3::new(
390        if relatives.is_x_relative() {
391            destination.x - current.x
392        } else {
393            destination.x
394        },
395        if relatives.is_y_relative() {
396            destination.y - current.y
397        } else {
398            destination.y
399        },
400        if relatives.is_z_relative() {
401            destination.z - current.z
402        } else {
403            destination.z
404        },
405    )
406}
407
408fn packet_rotation(
409    desired: (f32, f32),
410    current: (f32, f32),
411    relatives: RelativeMovement,
412) -> (f32, f32) {
413    wrap_rotation((
414        if relatives.is_y_rot_relative() {
415            desired.0 - current.0
416        } else {
417            desired.0
418        },
419        if relatives.is_x_rot_relative() {
420            desired.1 - current.1
421        } else {
422            desired.1
423        },
424    ))
425}
426
427fn wrap_rotation((yaw, pitch): (f32, f32)) -> (f32, f32) {
428    (wrap_degrees(yaw), wrap_degrees(pitch))
429}
430
431fn wrap_degrees(value: f32) -> f32 {
432    let wrapped = value.rem_euclid(360.0);
433    if wrapped >= 180.0 {
434        wrapped - 360.0
435    } else {
436        wrapped
437    }
438}
439
440fn send_entity_success(source: &CommandSource, targets: &[SharedEntity], destination: &dyn Entity) {
441    let message = if let [target] = targets {
442        translations::COMMANDS_TELEPORT_SUCCESS_ENTITY_SINGLE
443            .message([
444                TextComponent::plain(target.plain_text_name()),
445                TextComponent::plain(destination.plain_text_name()),
446            ])
447            .component()
448    } else {
449        translations::COMMANDS_TELEPORT_SUCCESS_ENTITY_MULTIPLE
450            .message([
451                TextComponent::plain(targets.len().to_string()),
452                TextComponent::plain(destination.plain_text_name()),
453            ])
454            .component()
455    };
456    source.send_success(&message, true);
457}
458
459fn send_position_success(source: &CommandSource, targets: &[SharedEntity], position: DVec3) {
460    let [x, y, z] = [
461        format!("{:.6}", position.x),
462        format!("{:.6}", position.y),
463        format!("{:.6}", position.z),
464    ];
465    let message = if let [target] = targets {
466        translations::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE
467            .message([
468                TextComponent::plain(target.plain_text_name()),
469                TextComponent::plain(x),
470                TextComponent::plain(y),
471                TextComponent::plain(z),
472            ])
473            .component()
474    } else {
475        translations::COMMANDS_TELEPORT_SUCCESS_LOCATION_MULTIPLE
476            .message([
477                TextComponent::plain(targets.len().to_string()),
478                TextComponent::plain(x),
479                TextComponent::plain(y),
480                TextComponent::plain(z),
481            ])
482            .component()
483    };
484    source.send_success(&message, true);
485}
486
487fn target_count(targets: &[SharedEntity]) -> Result<i32, CommandSyntaxError> {
488    i32::try_from(targets.len())
489        .map_err(|_| CommandSyntaxError::dynamic("Target count exceeds the command result range"))
490}
491
492fn missing_argument(name: &str) -> CommandSyntaxError {
493    CommandSyntaxError::dynamic(format!(
494        "Parsed value for {name} is missing from the command context"
495    ))
496}
497
498#[cfg(test)]
499mod tests {
500    use super::super::create_dispatcher;
501    use super::{ensure_same_domain, packet_position, packet_rotation, teleport_relatives};
502    use crate::command::{
503        brigadier::{CommandDispatcher, NodeId},
504        execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
505    };
506    use glam::DVec3;
507    use steel_protocol::packets::game::RelativeMovement;
508    use steel_registry::init_vanilla_registry;
509
510    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
511
512    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
513        let Some(children) = dispatcher.children(parent) else {
514            panic!("parent node should exist");
515        };
516        let Some(child) = children.iter().copied().find(|child| {
517            dispatcher
518                .node(*child)
519                .is_some_and(|node| node.name() == name)
520        }) else {
521            panic!("child {name} should exist");
522        };
523        child
524    }
525
526    #[test]
527    fn teleport_graph_matches_vanilla_target_and_facing_shapes() {
528        init_vanilla_registry();
529        let Ok(dispatcher) = create_dispatcher() else {
530            panic!("built-in commands should register");
531        };
532        for root_name in ["teleport", "tp"] {
533            let root = child(&dispatcher, dispatcher.root(), root_name);
534            let location = child(&dispatcher, root, "location");
535            assert!(matches!(
536                dispatcher.node(location),
537                Some(node)
538                    if node.is_executable()
539                        && node.argument_type() == Some(&SteelArgumentType::vec3(true))
540            ));
541
542            let destination = child(&dispatcher, root, "destination");
543            assert!(matches!(
544                dispatcher.node(destination),
545                Some(node)
546                    if node.is_executable()
547                        && node.argument_type() == Some(&SteelArgumentType::entity())
548            ));
549
550            let targets = child(&dispatcher, root, "targets");
551            let target_location = child(&dispatcher, targets, "location");
552            let rotation = child(&dispatcher, target_location, "rotation");
553            assert!(matches!(
554                dispatcher.node(rotation),
555                Some(node)
556                    if node.is_executable()
557                        && node.argument_type() == Some(&SteelArgumentType::rotation())
558            ));
559
560            let facing = child(&dispatcher, target_location, "facing");
561            let entity = child(&dispatcher, facing, "entity");
562            let facing_entity = child(&dispatcher, entity, "facingEntity");
563            let anchor = child(&dispatcher, facing_entity, "facingAnchor");
564            assert!(matches!(
565                dispatcher.node(anchor),
566                Some(node)
567                    if node.is_executable()
568                        && node.argument_type() == Some(&SteelArgumentType::entity_anchor())
569            ));
570            let facing_location = child(&dispatcher, facing, "facingLocation");
571            assert!(matches!(
572                dispatcher.node(facing_location),
573                Some(node) if node.is_executable()
574            ));
575        }
576    }
577
578    #[test]
579    fn different_worlds_strip_relative_position_but_preserve_direction_flags() {
580        let relatives = teleport_relatives((true, false, true), None, false);
581
582        assert!(!relatives.is_x_relative());
583        assert!(!relatives.is_z_relative());
584        assert_eq!(
585            relatives.0
586                & (RelativeMovement::DELTA_X
587                    | RelativeMovement::DELTA_Z
588                    | RelativeMovement::Y_ROT
589                    | RelativeMovement::X_ROT),
590            RelativeMovement::DELTA_X
591                | RelativeMovement::DELTA_Z
592                | RelativeMovement::Y_ROT
593                | RelativeMovement::X_ROT
594        );
595    }
596
597    #[test]
598    fn direct_teleport_rejects_cross_domain_transitions() {
599        assert!(ensure_same_domain("survival", "survival").is_ok());
600        assert!(ensure_same_domain("survival", "creative").is_err());
601    }
602
603    #[test]
604    fn packet_values_rebase_source_relative_results_for_each_target() {
605        let relatives = teleport_relatives((true, false, true), Some((true, false)), true);
606        assert_eq!(
607            packet_position(
608                DVec3::new(20.0, 64.0, 40.0),
609                DVec3::new(5.0, 10.0, 12.0),
610                relatives,
611            ),
612            DVec3::new(15.0, 64.0, 28.0)
613        );
614        assert_eq!(
615            packet_rotation((90.0, 30.0), (45.0, -10.0), relatives),
616            (45.0, 30.0)
617        );
618    }
619}