1use 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 }
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 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}