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_math::wrap_degrees;
7use steel_protocol::packets::game::{CSetCamera, RelativeMovement};
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 anchor = context.entity_anchor("facingAnchor")?;
118    teleport_targets_facing_entity(context, anchor)
119}
120
121fn teleport_targets_facing_entity(
122    context: &SteelCommandContext<CommandSource>,
123    anchor: EntityAnchor,
124) -> Result<i32, CommandSyntaxError> {
125    let targets = context.entities("targets")?;
126    let destination = required_coordinates(context, "location")?;
127    let facing_entity = context.entity("facingEntity")?;
128    teleport_to_position(
129        context,
130        &targets,
131        destination,
132        None,
133        Some(TeleportFacing::Entity {
134            target: facing_entity,
135            anchor,
136        }),
137    )
138}
139
140fn teleport_targets_facing_position(
141    context: &SteelCommandContext<CommandSource>,
142) -> Result<i32, CommandSyntaxError> {
143    let targets = context.entities("targets")?;
144    let destination = required_coordinates(context, "location")?;
145    let facing = required_coordinates(context, "facingLocation")?.position(context.source());
146    teleport_to_position(
147        context,
148        &targets,
149        destination,
150        None,
151        Some(TeleportFacing::Position(facing)),
152    )
153}
154
155fn teleport_targets_to_entity(
156    context: &SteelCommandContext<CommandSource>,
157) -> Result<i32, CommandSyntaxError> {
158    let targets = context.entities("targets")?;
159    let destination = context.entity("destination")?;
160    teleport_to_entity(context, &targets, &destination)
161}
162
163fn source_entity(
164    context: &SteelCommandContext<CommandSource>,
165) -> Result<&SharedEntity, CommandSyntaxError> {
166    context.source().entity().ok_or_else(|| {
167        CommandSyntaxError::dynamic(TextComponent::from(
168            &translations::PERMISSIONS_REQUIRES_ENTITY,
169        ))
170    })
171}
172
173fn required_coordinates(
174    context: &SteelCommandContext<CommandSource>,
175    name: &str,
176) -> Result<Coordinates, CommandSyntaxError> {
177    context.coordinates(name)
178}
179
180fn teleport_to_entity(
181    context: &SteelCommandContext<CommandSource>,
182    targets: &[SharedEntity],
183    destination: &SharedEntity,
184) -> Result<i32, CommandSyntaxError> {
185    let Some(target_world) = destination.level() else {
186        return Err(CommandSyntaxError::dynamic(
187            "Teleport destination is not in a live world",
188        ));
189    };
190    let position = destination.position();
191    ensure_spawnable_position(position)?;
192    let rotation = destination.rotation();
193    ensure_same_domain_targets(targets, &target_world)?;
194
195    for target in targets {
196        let transition = TeleportTransition {
197            target_world: Arc::clone(&target_world),
198            position,
199            rotation: wrap_rotation(rotation),
200            velocity: DVec3::ZERO,
201            relatives: RelativeMovement::NONE,
202            portal_cooldown: 0,
203            as_passenger: false,
204            post_transition: TeleportPostTransition::do_nothing(),
205        };
206        perform_teleport(context.source(), target, transition, None);
207    }
208
209    send_entity_success(context.source(), targets, destination.as_ref());
210    target_count(targets)
211}
212
213fn teleport_to_position(
214    context: &SteelCommandContext<CommandSource>,
215    targets: &[SharedEntity],
216    destination: Coordinates,
217    rotation: Option<Coordinates>,
218    facing: Option<TeleportFacing>,
219) -> Result<i32, CommandSyntaxError> {
220    let source = context.source();
221    let position = destination.position(source);
222    ensure_spawnable_position(position)?;
223    let resolved_rotation = rotation.map(|rotation| rotation.rotation(source));
224    ensure_same_domain_targets(targets, source.world())?;
225
226    for target in targets {
227        let same_world = target
228            .level()
229            .is_some_and(|world| Arc::ptr_eq(&world, source.world()));
230        let relatives = teleport_relatives(
231            (
232                destination.is_x_relative(),
233                destination.is_y_relative(),
234                destination.is_z_relative(),
235            ),
236            rotation.map(|rotation| (rotation.is_y_relative(), rotation.is_x_relative())),
237            same_world,
238        );
239        let target_rotation = target.rotation();
240        let desired_rotation = resolved_rotation.unwrap_or(target_rotation);
241        let transition = TeleportTransition {
242            target_world: Arc::clone(source.world()),
243            position: packet_position(position, target.position(), relatives),
244            rotation: packet_rotation(desired_rotation, target_rotation, relatives),
245            velocity: DVec3::ZERO,
246            relatives,
247            portal_cooldown: 0,
248            as_passenger: false,
249            post_transition: TeleportPostTransition::do_nothing(),
250        };
251        perform_teleport(source, target, transition, facing.as_ref());
252    }
253
254    send_position_success(source, targets, position);
255    target_count(targets)
256}
257
258enum TeleportFacing {
259    Position(DVec3),
260    Entity {
261        target: SharedEntity,
262        anchor: EntityAnchor,
263    },
264}
265
266fn perform_teleport(
267    source: &CommandSource,
268    target: &SharedEntity,
269    transition: TeleportTransition,
270    facing: Option<&TeleportFacing>,
271) {
272    if let Some(player) = target.as_player() {
273        if player.is_sleeping() {
274            player.stop_sleeping();
275        }
276        player.send_packet(CSetCamera {
277            camera_id: player.id(),
278        });
279    }
280    if change_entity_world(Arc::clone(target), &transition).is_none() {
281        return;
282    }
283
284    // Vanilla applies command post-effects through the original target reference.
285    if let Some(facing) = facing {
286        match facing {
287            TeleportFacing::Position(position) => target.look_at(source.anchor(), *position),
288            TeleportFacing::Entity {
289                target: facing_target,
290                anchor,
291            } => target.look_at_entity(source.anchor(), facing_target.as_ref(), *anchor),
292        }
293    }
294
295    if target
296        .as_living_entity()
297        .is_none_or(|living| !living.is_fall_flying())
298    {
299        let velocity = target.velocity();
300        target.set_velocity(DVec3::new(velocity.x, 0.0, velocity.z));
301        target.set_on_ground(true);
302    }
303    if let Some(pathfinder) = target.as_pathfinder_mob() {
304        pathfinder.mob_base().navigation().lock().stop();
305    }
306}
307
308fn ensure_spawnable_position(position: DVec3) -> Result<(), CommandSyntaxError> {
309    if World::is_in_spawnable_bounds(BlockPos::from(position)) {
310        return Ok(());
311    }
312    Err(CommandSyntaxError::dynamic(TextComponent::from(
313        &translations::COMMANDS_TELEPORT_INVALID_POSITION,
314    )))
315}
316
317fn ensure_same_domain_targets(
318    targets: &[SharedEntity],
319    target_world: &World,
320) -> Result<(), CommandSyntaxError> {
321    for target in targets {
322        let Some(source_world) = target.level() else {
323            continue;
324        };
325        ensure_same_domain(source_world.domain(), target_world.domain())?;
326    }
327    Ok(())
328}
329
330fn ensure_same_domain(source_domain: &str, target_domain: &str) -> Result<(), CommandSyntaxError> {
331    if source_domain == target_domain {
332        return Ok(());
333    }
334    Err(CommandSyntaxError::dynamic(
335        "Entities cannot be teleported across Steel domains",
336    ))
337}
338
339fn teleport_relatives(
340    position_relative: (bool, bool, bool),
341    rotation_relative: Option<(bool, bool)>,
342    same_world: bool,
343) -> RelativeMovement {
344    let mut flags = 0;
345    let (relative_x, relative_y, relative_z) = position_relative;
346    if relative_x {
347        flags |= RelativeMovement::DELTA_X;
348        if same_world {
349            flags |= RelativeMovement::X;
350        }
351    }
352    if relative_y {
353        flags |= RelativeMovement::DELTA_Y;
354        if same_world {
355            flags |= RelativeMovement::Y;
356        }
357    }
358    if relative_z {
359        flags |= RelativeMovement::DELTA_Z;
360        if same_world {
361            flags |= RelativeMovement::Z;
362        }
363    }
364
365    let (relative_yaw, relative_pitch) = rotation_relative.unwrap_or((true, true));
366    if relative_yaw {
367        flags |= RelativeMovement::Y_ROT;
368    }
369    if relative_pitch {
370        flags |= RelativeMovement::X_ROT;
371    }
372    RelativeMovement::new(flags)
373}
374
375fn packet_position(destination: DVec3, current: DVec3, relatives: RelativeMovement) -> DVec3 {
376    DVec3::new(
377        if relatives.is_x_relative() {
378            destination.x - current.x
379        } else {
380            destination.x
381        },
382        if relatives.is_y_relative() {
383            destination.y - current.y
384        } else {
385            destination.y
386        },
387        if relatives.is_z_relative() {
388            destination.z - current.z
389        } else {
390            destination.z
391        },
392    )
393}
394
395fn packet_rotation(
396    desired: (f32, f32),
397    current: (f32, f32),
398    relatives: RelativeMovement,
399) -> (f32, f32) {
400    wrap_rotation((
401        if relatives.is_y_rot_relative() {
402            desired.0 - current.0
403        } else {
404            desired.0
405        },
406        if relatives.is_x_rot_relative() {
407            desired.1 - current.1
408        } else {
409            desired.1
410        },
411    ))
412}
413
414fn wrap_rotation((yaw, pitch): (f32, f32)) -> (f32, f32) {
415    (wrap_degrees(yaw), wrap_degrees(pitch))
416}
417
418fn send_entity_success(source: &CommandSource, targets: &[SharedEntity], destination: &dyn Entity) {
419    let message = if let [target] = targets {
420        translations::COMMANDS_TELEPORT_SUCCESS_ENTITY_SINGLE
421            .message([
422                TextComponent::plain(target.plain_text_name()),
423                TextComponent::plain(destination.plain_text_name()),
424            ])
425            .component()
426    } else {
427        translations::COMMANDS_TELEPORT_SUCCESS_ENTITY_MULTIPLE
428            .message([
429                TextComponent::plain(targets.len().to_string()),
430                TextComponent::plain(destination.plain_text_name()),
431            ])
432            .component()
433    };
434    source.send_success(&message, true);
435}
436
437fn send_position_success(source: &CommandSource, targets: &[SharedEntity], position: DVec3) {
438    let [x, y, z] = [
439        format!("{:.6}", position.x),
440        format!("{:.6}", position.y),
441        format!("{:.6}", position.z),
442    ];
443    let message = if let [target] = targets {
444        translations::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE
445            .message([
446                TextComponent::plain(target.plain_text_name()),
447                TextComponent::plain(x),
448                TextComponent::plain(y),
449                TextComponent::plain(z),
450            ])
451            .component()
452    } else {
453        translations::COMMANDS_TELEPORT_SUCCESS_LOCATION_MULTIPLE
454            .message([
455                TextComponent::plain(targets.len().to_string()),
456                TextComponent::plain(x),
457                TextComponent::plain(y),
458                TextComponent::plain(z),
459            ])
460            .component()
461    };
462    source.send_success(&message, true);
463}
464
465fn target_count(targets: &[SharedEntity]) -> Result<i32, CommandSyntaxError> {
466    i32::try_from(targets.len())
467        .map_err(|_| CommandSyntaxError::dynamic("Target count exceeds the command result range"))
468}
469
470#[cfg(test)]
471mod tests {
472    use super::super::create_dispatcher;
473    use super::{ensure_same_domain, packet_position, packet_rotation, teleport_relatives};
474    use crate::command::{
475        brigadier::{CommandDispatcher, NodeId},
476        execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
477    };
478    use glam::DVec3;
479    use steel_protocol::packets::game::RelativeMovement;
480    use steel_registry::init_vanilla_registry;
481
482    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
483
484    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
485        let Some(children) = dispatcher.children(parent) else {
486            panic!("parent node should exist");
487        };
488        let Some(child) = children.iter().copied().find(|child| {
489            dispatcher
490                .node(*child)
491                .is_some_and(|node| node.name() == name)
492        }) else {
493            panic!("child {name} should exist");
494        };
495        child
496    }
497
498    #[test]
499    fn teleport_graph_matches_vanilla_target_and_facing_shapes() {
500        init_vanilla_registry();
501        let Ok(dispatcher) = create_dispatcher() else {
502            panic!("built-in commands should register");
503        };
504        for root_name in ["teleport", "tp"] {
505            let root = child(&dispatcher, dispatcher.root(), root_name);
506            let location = child(&dispatcher, root, "location");
507            assert!(matches!(
508                dispatcher.node(location),
509                Some(node)
510                    if node.is_executable()
511                        && node.argument_type() == Some(&SteelArgumentType::vec3(true))
512            ));
513
514            let destination = child(&dispatcher, root, "destination");
515            assert!(matches!(
516                dispatcher.node(destination),
517                Some(node)
518                    if node.is_executable()
519                        && node.argument_type() == Some(&SteelArgumentType::entity())
520            ));
521
522            let targets = child(&dispatcher, root, "targets");
523            let target_location = child(&dispatcher, targets, "location");
524            let rotation = child(&dispatcher, target_location, "rotation");
525            assert!(matches!(
526                dispatcher.node(rotation),
527                Some(node)
528                    if node.is_executable()
529                        && node.argument_type() == Some(&SteelArgumentType::rotation())
530            ));
531
532            let facing = child(&dispatcher, target_location, "facing");
533            let entity = child(&dispatcher, facing, "entity");
534            let facing_entity = child(&dispatcher, entity, "facingEntity");
535            let anchor = child(&dispatcher, facing_entity, "facingAnchor");
536            assert!(matches!(
537                dispatcher.node(anchor),
538                Some(node)
539                    if node.is_executable()
540                        && node.argument_type() == Some(&SteelArgumentType::entity_anchor())
541            ));
542            let facing_location = child(&dispatcher, facing, "facingLocation");
543            assert!(matches!(
544                dispatcher.node(facing_location),
545                Some(node) if node.is_executable()
546            ));
547        }
548    }
549
550    #[test]
551    fn different_worlds_strip_relative_position_but_preserve_direction_flags() {
552        let relatives = teleport_relatives((true, false, true), None, false);
553
554        assert!(!relatives.is_x_relative());
555        assert!(!relatives.is_z_relative());
556        assert_eq!(
557            relatives.0
558                & (RelativeMovement::DELTA_X
559                    | RelativeMovement::DELTA_Z
560                    | RelativeMovement::Y_ROT
561                    | RelativeMovement::X_ROT),
562            RelativeMovement::DELTA_X
563                | RelativeMovement::DELTA_Z
564                | RelativeMovement::Y_ROT
565                | RelativeMovement::X_ROT
566        );
567    }
568
569    #[test]
570    fn direct_teleport_rejects_cross_domain_transitions() {
571        assert!(ensure_same_domain("survival", "survival").is_ok());
572        assert!(ensure_same_domain("survival", "creative").is_err());
573    }
574
575    #[test]
576    fn packet_values_rebase_source_relative_results_for_each_target() {
577        let relatives = teleport_relatives((true, false, true), Some((true, false)), true);
578        assert_eq!(
579            packet_position(
580                DVec3::new(20.0, 64.0, 40.0),
581                DVec3::new(5.0, 10.0, 12.0),
582                relatives,
583            ),
584            DVec3::new(15.0, 64.0, 28.0)
585        );
586        assert_eq!(
587            packet_rotation((90.0, 30.0), (45.0, -10.0), relatives),
588            (45.0, 30.0)
589        );
590    }
591}