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::{CSetCamera, RelativeMovement};
7use steel_utils::{Identifier, translations};
8use text_components::TextComponent;
9
10use super::super::{
11    brigadier::{CommandNodeBuilder, CommandSyntaxError},
12    execution::{
13        CommandSource, Coordinates, SteelArgumentType, SteelCommandContext, SteelCommandRuntime,
14        argument, literal,
15    },
16    registration::CommandRegistration,
17};
18use super::position::{ensure_spawnable_position, finite_rotation, wrap_rotation};
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.map_or(target_rotation, |rotation| {
241            finite_rotation(rotation, target_rotation)
242        });
243        let transition = TeleportTransition {
244            target_world: Arc::clone(source.world()),
245            position: packet_position(position, target.position(), relatives),
246            rotation: packet_rotation(desired_rotation, target_rotation, relatives),
247            velocity: DVec3::ZERO,
248            relatives,
249            portal_cooldown: 0,
250            as_passenger: false,
251            post_transition: TeleportPostTransition::do_nothing(),
252        };
253        perform_teleport(source, target, transition, facing.as_ref());
254    }
255
256    send_position_success(source, targets, position);
257    target_count(targets)
258}
259
260enum TeleportFacing {
261    Position(DVec3),
262    Entity {
263        target: SharedEntity,
264        anchor: EntityAnchor,
265    },
266}
267
268fn perform_teleport(
269    source: &CommandSource,
270    target: &SharedEntity,
271    transition: TeleportTransition,
272    facing: Option<&TeleportFacing>,
273) {
274    if let Some(player) = target.as_player() {
275        if player.is_sleeping() {
276            player.stop_sleeping();
277        }
278        player.send_packet(CSetCamera {
279            camera_id: player.id(),
280        });
281    }
282    if change_entity_world(Arc::clone(target), &transition).is_none() {
283        return;
284    }
285
286    // Vanilla applies command post-effects through the original target reference.
287    if let Some(facing) = facing {
288        match facing {
289            TeleportFacing::Position(position) => target.look_at(source.anchor(), *position),
290            TeleportFacing::Entity {
291                target: facing_target,
292                anchor,
293            } => target.look_at_entity(source.anchor(), facing_target.as_ref(), *anchor),
294        }
295    }
296
297    target.land_after_teleport();
298    if let Some(pathfinder) = target.as_pathfinder_mob() {
299        pathfinder.mob_base().navigation().lock().stop();
300    }
301}
302
303fn ensure_same_domain_targets(
304    targets: &[SharedEntity],
305    target_world: &World,
306) -> Result<(), CommandSyntaxError> {
307    for target in targets {
308        let Some(source_world) = target.level() else {
309            continue;
310        };
311        ensure_same_domain(source_world.domain(), target_world.domain())?;
312    }
313    Ok(())
314}
315
316fn ensure_same_domain(source_domain: &str, target_domain: &str) -> Result<(), CommandSyntaxError> {
317    if source_domain == target_domain {
318        return Ok(());
319    }
320    Err(CommandSyntaxError::dynamic(
321        "Entities cannot be teleported across Steel domains",
322    ))
323}
324
325fn teleport_relatives(
326    position_relative: (bool, bool, bool),
327    rotation_relative: Option<(bool, bool)>,
328    same_world: bool,
329) -> RelativeMovement {
330    let mut flags = 0;
331    let (relative_x, relative_y, relative_z) = position_relative;
332    if relative_x {
333        flags |= RelativeMovement::DELTA_X;
334        if same_world {
335            flags |= RelativeMovement::X;
336        }
337    }
338    if relative_y {
339        flags |= RelativeMovement::DELTA_Y;
340        if same_world {
341            flags |= RelativeMovement::Y;
342        }
343    }
344    if relative_z {
345        flags |= RelativeMovement::DELTA_Z;
346        if same_world {
347            flags |= RelativeMovement::Z;
348        }
349    }
350
351    let (relative_yaw, relative_pitch) = rotation_relative.unwrap_or((true, true));
352    if relative_yaw {
353        flags |= RelativeMovement::Y_ROT;
354    }
355    if relative_pitch {
356        flags |= RelativeMovement::X_ROT;
357    }
358    RelativeMovement::new(flags)
359}
360
361fn packet_position(destination: DVec3, current: DVec3, relatives: RelativeMovement) -> DVec3 {
362    DVec3::new(
363        if relatives.is_x_relative() {
364            destination.x - current.x
365        } else {
366            destination.x
367        },
368        if relatives.is_y_relative() {
369            destination.y - current.y
370        } else {
371            destination.y
372        },
373        if relatives.is_z_relative() {
374            destination.z - current.z
375        } else {
376            destination.z
377        },
378    )
379}
380
381fn packet_rotation(
382    desired: (f32, f32),
383    current: (f32, f32),
384    relatives: RelativeMovement,
385) -> (f32, f32) {
386    wrap_rotation((
387        if relatives.is_y_rot_relative() {
388            desired.0 - current.0
389        } else {
390            desired.0
391        },
392        if relatives.is_x_rot_relative() {
393            desired.1 - current.1
394        } else {
395            desired.1
396        },
397    ))
398}
399
400fn send_entity_success(source: &CommandSource, targets: &[SharedEntity], destination: &dyn Entity) {
401    let message = if let [target] = targets {
402        translations::COMMANDS_TELEPORT_SUCCESS_ENTITY_SINGLE
403            .message([
404                TextComponent::plain(target.plain_text_name()),
405                TextComponent::plain(destination.plain_text_name()),
406            ])
407            .component()
408    } else {
409        translations::COMMANDS_TELEPORT_SUCCESS_ENTITY_MULTIPLE
410            .message([
411                TextComponent::plain(targets.len().to_string()),
412                TextComponent::plain(destination.plain_text_name()),
413            ])
414            .component()
415    };
416    source.send_success(&message, true);
417}
418
419fn send_position_success(source: &CommandSource, targets: &[SharedEntity], position: DVec3) {
420    let [x, y, z] = [
421        format!("{:.6}", position.x),
422        format!("{:.6}", position.y),
423        format!("{:.6}", position.z),
424    ];
425    let message = if let [target] = targets {
426        translations::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE
427            .message([
428                TextComponent::plain(target.plain_text_name()),
429                TextComponent::plain(x),
430                TextComponent::plain(y),
431                TextComponent::plain(z),
432            ])
433            .component()
434    } else {
435        translations::COMMANDS_TELEPORT_SUCCESS_LOCATION_MULTIPLE
436            .message([
437                TextComponent::plain(targets.len().to_string()),
438                TextComponent::plain(x),
439                TextComponent::plain(y),
440                TextComponent::plain(z),
441            ])
442            .component()
443    };
444    source.send_success(&message, true);
445}
446
447fn target_count(targets: &[SharedEntity]) -> Result<i32, CommandSyntaxError> {
448    i32::try_from(targets.len())
449        .map_err(|_| CommandSyntaxError::dynamic("Target count exceeds the command result range"))
450}
451
452#[cfg(test)]
453mod tests {
454    use super::super::create_dispatcher;
455    use super::{ensure_same_domain, packet_position, packet_rotation, teleport_relatives};
456    use crate::command::{
457        brigadier::{CommandDispatcher, NodeId},
458        execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
459    };
460    use glam::DVec3;
461    use steel_protocol::packets::game::RelativeMovement;
462    use steel_registry::init_vanilla_registry;
463
464    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
465
466    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
467        let Some(children) = dispatcher.children(parent) else {
468            panic!("parent node should exist");
469        };
470        let Some(child) = children.iter().copied().find(|child| {
471            dispatcher
472                .node(*child)
473                .is_some_and(|node| node.name() == name)
474        }) else {
475            panic!("child {name} should exist");
476        };
477        child
478    }
479
480    #[test]
481    fn teleport_graph_matches_vanilla_target_and_facing_shapes() {
482        init_vanilla_registry();
483        let Ok(dispatcher) = create_dispatcher() else {
484            panic!("built-in commands should register");
485        };
486        for root_name in ["teleport", "tp"] {
487            let root = child(&dispatcher, dispatcher.root(), root_name);
488            let location = child(&dispatcher, root, "location");
489            assert!(matches!(
490                dispatcher.node(location),
491                Some(node)
492                    if node.is_executable()
493                        && node.argument_type() == Some(&SteelArgumentType::vec3(true))
494            ));
495
496            let destination = child(&dispatcher, root, "destination");
497            assert!(matches!(
498                dispatcher.node(destination),
499                Some(node)
500                    if node.is_executable()
501                        && node.argument_type() == Some(&SteelArgumentType::entity())
502            ));
503
504            let targets = child(&dispatcher, root, "targets");
505            let target_location = child(&dispatcher, targets, "location");
506            let rotation = child(&dispatcher, target_location, "rotation");
507            assert!(matches!(
508                dispatcher.node(rotation),
509                Some(node)
510                    if node.is_executable()
511                        && node.argument_type() == Some(&SteelArgumentType::rotation())
512            ));
513
514            let facing = child(&dispatcher, target_location, "facing");
515            let entity = child(&dispatcher, facing, "entity");
516            let facing_entity = child(&dispatcher, entity, "facingEntity");
517            let anchor = child(&dispatcher, facing_entity, "facingAnchor");
518            assert!(matches!(
519                dispatcher.node(anchor),
520                Some(node)
521                    if node.is_executable()
522                        && node.argument_type() == Some(&SteelArgumentType::entity_anchor())
523            ));
524            let facing_location = child(&dispatcher, facing, "facingLocation");
525            assert!(matches!(
526                dispatcher.node(facing_location),
527                Some(node) if node.is_executable()
528            ));
529        }
530    }
531
532    #[test]
533    fn different_worlds_strip_relative_position_but_preserve_direction_flags() {
534        let relatives = teleport_relatives((true, false, true), None, false);
535
536        assert!(!relatives.is_x_relative());
537        assert!(!relatives.is_z_relative());
538        assert_eq!(
539            relatives.0
540                & (RelativeMovement::DELTA_X
541                    | RelativeMovement::DELTA_Z
542                    | RelativeMovement::Y_ROT
543                    | RelativeMovement::X_ROT),
544            RelativeMovement::DELTA_X
545                | RelativeMovement::DELTA_Z
546                | RelativeMovement::Y_ROT
547                | RelativeMovement::X_ROT
548        );
549    }
550
551    #[test]
552    fn direct_teleport_rejects_cross_domain_transitions() {
553        assert!(ensure_same_domain("survival", "survival").is_ok());
554        assert!(ensure_same_domain("survival", "creative").is_err());
555    }
556
557    #[test]
558    fn packet_values_rebase_source_relative_results_for_each_target() {
559        let relatives = teleport_relatives((true, false, true), Some((true, false)), true);
560        assert_eq!(
561            packet_position(
562                DVec3::new(20.0, 64.0, 40.0),
563                DVec3::new(5.0, 10.0, 12.0),
564                relatives,
565            ),
566            DVec3::new(15.0, 64.0, 28.0)
567        );
568        assert_eq!(
569            packet_rotation((90.0, 30.0), (45.0, -10.0), relatives),
570            (45.0, 30.0)
571        );
572    }
573}