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