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