Carry vehicle passengers through command dimension teleports

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
voidsipandCodex committed 2026-11-04 14:48:13 -06:00
1 parent 1989b1b595
commit 8f5f91f2f0
10 files changed
+438 -25

No files matched your search

+40 -18
View File
@@ -1516,7 +1516,7 @@ impl Game<'_> {
// An entity in another level is teleported in the level that holds it.
let _ = self.in_target_level(t, |g| g.teleport_to_in_level(t, pos, rotation, turn_riders));
}
fn teleport_to_in_level(
pub(super) fn teleport_to_in_level(
&mut self,
t: Target,
pos: [f64; 3],
@@ -1779,7 +1779,16 @@ impl Game<'_> {
level: super::Dim,
pos: [f64; 3],
rotation: Option<[f32; 2]>,
) -> bool {
turn_riders: bool,
) -> R<bool> {
match self.teleport_vehicle_across(t, level, pos, rotation, turn_riders) {
Ok(true) => return Ok(true),
Ok(false) => {}
Err(error) => {
log::error!("entity teleport into {}: {error}", level.id());
return Err(err("commands.generic.exception", &[]));
}
}
let conn = match t {
Target::Player => self.lan_acting,
Target::Guest(conn) => Some(conn),
@@ -1787,7 +1796,7 @@ impl Game<'_> {
};
let at = self.target_level(t);
let Some(conn) = conn.filter(|_| at != level) else {
return false;
return Ok(false);
};
self.level_teleports
.push(super::server_travel::LevelTeleport {
@@ -1795,8 +1804,9 @@ impl Game<'_> {
dim: level,
at: pos,
rotation,
vehicle: None,
});
true
Ok(true)
}
/// `ServerPlayer.lookAt`: besides turning the player, tell their client
/// with a `ClientboundPlayerLookAtPacket` (`from` anchor, the point, and
@@ -1854,13 +1864,22 @@ impl Game<'_> {
};
if id.ends_with("destination") {
let dest = self.entities(ctx, "destination", false)?[0];
let pos = self.target_position(dest).unwrap_or(self.source_position());
// Into the destination's level.
let level = self.target_level(dest);
// TeleportCommand.teleportToEntity: the destination's rotation too.
let rotation = Some(self.target_rotation(dest).map(wrap_degrees));
let (pos, level, rotation, destination_name) = self
.in_target_level(dest, |game| {
(
game.target_position(dest).unwrap_or(game.source_position()),
game.target_level(dest),
Some(game.target_rotation(dest).map(wrap_degrees)),
game.target_name(dest),
)
})
.ok_or_else(|| err("argument.entity.notfound.entity", &[]))?;
let target_name = (targets.len() == 1).then(|| {
self.in_target_level(targets[0], |game| game.target_name(targets[0]))
.unwrap_or_else(Text::empty)
});
for &t in &targets {
if !self.teleport_across(t, level, pos, rotation) {
if !self.teleport_across(t, level, pos, rotation, true)? {
// TeleportCommand.teleportToEntity: the destination's angles,
// which the riders turn to as well.
self.teleport_to(t, pos, rotation, true);
@@ -1869,15 +1888,12 @@ impl Game<'_> {
let message = if targets.len() == 1 {
Text::translate(
"commands.teleport.success.entity.single",
&[self.target_name(targets[0]), self.target_name(dest)],
&[target_name.unwrap(), destination_name],
)
} else {
Text::translate(
"commands.teleport.success.entity.multiple",
&[
Text::literal(targets.len().to_string()),
self.target_name(dest),
],
&[Text::literal(targets.len().to_string()), destination_name],
)
};
self.feedback(message);
@@ -1940,6 +1956,10 @@ impl Game<'_> {
None if self.source_entity().is_none() => super::Dim::Overworld,
None => self.dim,
};
let target_name = (targets.len() == 1).then(|| {
self.in_target_level(targets[0], |game| game.target_name(targets[0]))
.unwrap_or_else(Text::empty)
});
for &t in &targets {
// `facing` teleports with the rotation unchanged (Relative.ROTATION,
// no turn for the riders), then LookAt.perform turns the target alone.
@@ -1947,7 +1967,9 @@ impl Game<'_> {
let turn_riders = looked.is_none();
let rotation = looked.or_else(|| {
let (angles, relative) = rotation?;
let current = self.target_rotation(t);
let current = self
.in_target_level(t, |game| game.target_rotation(t))
.unwrap_or([0.0, 0.0]);
// performTeleport wraps relative deltas before calculateAbsolute
// adds them back; wrapping the resolved pitch changes its clamp.
Some(std::array::from_fn(|axis| {
@@ -1958,7 +1980,7 @@ impl Game<'_> {
}
}))
});
if self.teleport_across(t, source_level, pos, rotation) {
if self.teleport_across(t, source_level, pos, rotation, turn_riders)? {
continue;
}
self.teleport_to(t, pos, rotation, turn_riders);
@@ -1971,7 +1993,7 @@ impl Game<'_> {
let message = if targets.len() == 1 {
Text::translate(
"commands.teleport.success.location.single",
&[self.target_name(targets[0]), x, y, z],
&[target_name.unwrap(), x, y, z],
)
} else {
Text::translate(
@@ -134,12 +134,12 @@ impl Game<'_> {
.boats
.iter()
.find(|b| b.id == id)
.map_or([0.0; 2], |b| [b.yaw, 0.0]),
.map_or([0.0; 2], |b| [b.yaw, b.pitch]),
Target::Cart(id) => self
.carts
.iter()
.find(|c| c.id == id)
.map_or([0.0; 2], |c| [c.yaw, 0.0]),
.map_or([0.0; 2], |c| [c.yaw, c.pitch]),
Target::Fangs(id) => self
.fangs
.list
@@ -1460,6 +1460,14 @@ impl Game<'_> {
}
self.riding = Some(id);
self.yaw = self.boats[i].yaw;
// ServerPlayer.startRiding seats the player immediately.
let at = self.player_boat_seat(i);
if let Some(player) = self.player.as_mut() {
player.position = at;
player.velocity = horizon_core::geometry::Vec3::ZERO;
player.teleported = true;
}
self.previous_position = at;
// sendEventAndTriggers: START_RIDING_TRIGGER.
self.trigger_player("minecraft:started_riding");
}
@@ -1551,11 +1559,22 @@ impl Game<'_> {
return;
}
self.mobs[i].extra.mount = None;
let remaining = self
.mobs
.iter()
.find(|m| m.extra.mount == Some(v))
.map(|m| m.id);
if let Some(vehicle) = self.mobs.iter_mut().find(|m| m.id == v) {
vehicle.extra.rider = remaining;
}
let (width, yaw) = (f64::from(self.mobs[i].width()), self.mobs[i].yaw);
let at = if let Some(b) = self.boats.iter().position(|b| b.id == v) {
self.boat_dismount_target(b, width, yaw)
} else if let Some(at) = self.horse_dismount_point(v, i) {
at
} else if let Some(at) = self.cart_dismount_point(v, i) {
at
} else if let Some(m) = self.mobs.iter_mut().find(|m| m.id == v) {
m.extra.rider = None;
// Entity.getDismountLocationForPassenger: on top of the vehicle.
let p = m.position;
horizon_core::geometry::Vec3::new(p.x, p.y + f64::from(m.height()), p.z)
@@ -1033,6 +1033,10 @@ impl Game<'_> {
}
if g.vehicle.is_none() && (moved > 0.0001 || turned || teleported) {
host.hub.teleport(i, Some(at));
} else if teleported {
// ServerPlayer.startRiding and an asPassenger transition also
// teleport the rider. Retain its mount and publish its body.
host.hub.teleport_rider(i, at);
}
}
let g = &mut host.hub.guests[i];
@@ -2837,6 +2841,7 @@ impl Game<'_> {
dim,
at,
rotation: Some([yaw, pitch]),
vehicle: None,
});
return;
}
@@ -2787,6 +2787,8 @@ impl Minecart {
c.yaw = yaw;
c.previous_yaw = yaw;
c.no_gravity = horizon_client::entity_nbt::flag(tag, "NoGravity");
c.pitch = horizon_client::entity_nbt::rotation(tag)[1];
c.previous_pitch = c.pitch;
let int = |k: &str| match tag.get(k) {
Some(Tag::Byte(v)) => Some(i32::from(*v)),
Some(Tag::Short(v)) => Some(i32::from(*v)),
+3 -2
View File
@@ -671,6 +671,7 @@ impl Game<'_> {
let (wood, chest) = super::boats::any_boat_wood(k).unwrap_or(("oak", false));
let mut b = super::boats::Boat::new(id, std::sync::Arc::from(wood), position, yaw);
b.no_gravity = entity_nbt::flag(&tag, "NoGravity");
b.pitch = entity_nbt::rotation(&tag)[1];
if chest {
b = b.with_chest();
b.items =
@@ -970,7 +971,7 @@ impl Game<'_> {
there.then_some(v)
}
/// The root vehicle `root` with its passengers (`Entity.save`).
fn vehicle_tree_nbt(&mut self, root: u32) -> Option<Tag> {
pub(super) fn vehicle_tree_nbt(&mut self, root: u32) -> Option<Tag> {
if let Some(i) = self.mobs.iter().position(|m| m.id == root) {
return Some(self.mob_tree_nbt(i));
}
@@ -1881,7 +1882,7 @@ fn root_vehicle_tag(attach: [i32; 4], entity: Tag) -> Tag {
])
}
/// `Entity.getPassengersAndSelf` of `root`, from each rider's mount.
fn vehicle_tree(root: u32, mounts: &[(u32, Option<u32>)]) -> Vec<u32> {
pub(super) fn vehicle_tree(root: u32, mounts: &[(u32, Option<u32>)]) -> Vec<u32> {
let mut tree = vec![root];
let mut k = 0;
while k < tree.len() {
@@ -140,6 +140,8 @@ pub(super) struct LevelTeleport {
pub dim: Dim,
pub at: [f64; 3],
pub rotation: Option<[f32; 2]>,
/// `TeleportTransition.asPassenger`: the vehicle already moved with its tree.
pub vehicle: Option<u32>,
}
/// A teleport a command made inside a level: told to the mods once it has
@@ -608,6 +610,7 @@ impl Game<'_> {
dim: Dim::Overworld,
at,
rotation: None,
vehicle: None,
});
self.run_level_teleports(host);
self.world_spawn_trip(conn);
@@ -858,11 +861,22 @@ impl Game<'_> {
let origin = g.position;
// A new body there: no fall distance or motion carried over, but
// the player's attributes (ServerPlayer.teleport keeps the entity).
g.new_body(true);
let passenger_body = if t.vehicle.is_some() {
g.motion.take()
} else {
g.new_body(true);
None
};
let cooldown = host.play.get(&t.conn).map_or(0, |p| p.portal.cooldown);
let ctx = self.server_ctx();
host.hub
.change_level(i, t.at, yaw, Some(pitch), false, cooldown, &ctx);
let g = &mut host.hub.guests[i];
g.vehicle = t.vehicle;
if let Some(mut body) = passenger_body {
body.position = Vec3::new(t.at[0], t.at[1], t.at[2]);
g.motion = Some(body);
}
self.as_guest(host, t.conn, |g| {
g.trigger_dimension(source, t.dim, origin);
// The mods hear the teleport into another level, with the
@@ -9,6 +9,22 @@ use super::commands::Target;
use horizon_core::geometry::Vec3;
use horizon_protocol::packets::play::{self, Clientbound as C};
fn is_horse(species: horizon_client::mobs::Species) -> bool {
use horizon_client::mobs::Species::*;
matches!(
species,
Horse
| Donkey
| Mule
| SkeletonHorse
| ZombieHorse
| Camel
| CamelHusk
| Llama
| TraderLlama
)
}
/// `Entity.calculatePassengerTransition` with no relative axes: the passenger
/// keeps its offset from the vehicle.
pub(super) fn passenger_destination(old_vehicle: Vec3, new_vehicle: Vec3, passenger: Vec3) -> Vec3 {
@@ -61,6 +77,279 @@ fn vehicle_id(t: Target) -> Option<u32> {
}
impl Game<'_> {
/// `AbstractMinecart.getDismountLocationForPassenger` for a standing mob.
pub(super) fn cart_dismount_point(&self, vehicle: u32, rider: usize) -> Option<Vec3> {
use horizon_core::geometry::Aabb;
let cart = self.carts.iter().find(|c| c.id == vehicle)?;
let rider = &self.mobs[rider];
let direction =
(f64::from(cart.yaw) / 90.0 + 0.5).floor() as i32 + if cart.flipped { 3 } else { 1 };
let (fx, fz) = [(0, 1), (-1, 0), (0, -1), (1, 0)][(direction & 3) as usize];
let (rx, rz) = (-fz, fx);
let offsets = [
(rx, rz),
(-rx, -rz),
(rx - fx, rz - fz),
(-rx - fx, -rz - fz),
(fx + rx, fz + rz),
(fx - rx, fz - rz),
(-fx, -fz),
(fx, fz),
];
let [bx, by, bz] =
[cart.position.x, cart.position.y, cart.position.z].map(|v| v.floor() as i32);
let half = f64::from(rider.width().min(1.0) / 2.0);
for dy in [0, 1, -1] {
for (dx, dz) in offsets {
let p = [bx + dx, by + dy, bz + dz];
let shape = self.block_collision_shape(p);
let below = self.block_collision_shape([p[0], p[1] - 1, p[2]]);
let floor = horizon_core::geometry::block_floor_height(&shape, || &below);
if !floor.is_infinite() && floor < 1.0 {
let at = Vec3::new(
f64::from(p[0]) + 0.5,
f64::from(p[1]) + floor,
f64::from(p[2]) + 0.5,
);
let bounds = Aabb::new(
Vec3::new(at.x - half, at.y, at.z - half),
Vec3::new(at.x + half, at.y + f64::from(rider.height()), at.z + half),
);
if self.can_dismount_to(bounds) {
return Some(at);
}
}
}
}
Some(Vec3::new(
cart.position.x,
cart.position.y + super::minecarts::HEIGHT,
cart.position.z,
))
}
/// `AbstractHorse.getDismountLocationForPassenger`: try either side
/// through the mount's height, then fall back to its feet.
pub(super) fn horse_dismount_point(&self, vehicle: u32, rider: usize) -> Option<Vec3> {
use horizon_core::geometry::{Aabb, trig};
let mount = self
.mobs
.iter()
.find(|m| m.id == vehicle && is_horse(m.species()))?;
let rider = &self.mobs[rider];
let turn = if rider.main_arm_left() { -90.0 } else { 90.0 };
let reach =
(f64::from(mount.width()) + f64::from(rider.width()) + f64::from(1e-5_f32)) / 2.0;
let limit = mount.bounds().max_position().y + 0.75;
for turn in [turn, -turn] {
let radians = f64::from((mount.yaw + turn) * 0.017_453_292);
let (dx, dz) = (-trig::sin(radians), trig::cos(radians));
let scale = f64::from(dx.abs().max(dz.abs()));
let x = mount.position.x + f64::from(dx) * reach / scale;
let z = mount.position.z + f64::from(dz) * reach / scale;
let mut y = mount.position.y.floor() as i32;
loop {
let p = [x.floor() as i32, y, z.floor() as i32];
let shape = self.block_collision_shape(p);
let below = self.block_collision_shape([p[0], p[1] - 1, p[2]]);
let floor = horizon_core::geometry::block_floor_height(&shape, || &below);
if f64::from(y) + floor > limit {
break;
}
if !floor.is_infinite() && floor < 1.0 {
let at = Vec3::new(x, f64::from(y) + floor, z);
let half = f64::from(rider.width()) / 2.0;
let bounds = Aabb::new(
Vec3::new(x - half, at.y, z - half),
Vec3::new(x + half, at.y + f64::from(rider.height()), z + half),
);
if self.can_dismount_to(bounds) {
return Some(at);
}
}
y += 1;
if f64::from(y) >= limit {
break;
}
}
}
Some(mount.position)
}
/// `Entity.teleportCrossDimension` ejects the riders before calculating
/// their transitions. Dismounting also seats their own passengers again.
fn prepare_vehicle_transition(&mut self, root: u32) {
let riders: Vec<_> = self
.mobs
.iter()
.filter(|m| m.extra.mount == Some(root))
.map(|m| m.id)
.collect();
for &rider in riders.iter().rev() {
self.dismount_mob(rider);
self.snap_mob_passengers(rider);
}
for rider in riders {
self.prepare_vehicle_transition(rider);
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == rider) {
// Preserve the tree for serialization after all transitions
// have used the positions left by ejectPassengers.
m.extra.mount = Some(root);
m.extra.passenger = true;
}
}
}
/// `Entity.teleportPassengers` on a mob dismounted during a transition.
fn snap_mob_passengers(&mut self, root: u32) {
use horizon_client::mobs::riders;
let Some(vehicle) = self.mobs.iter().find(|m| m.id == root) else {
return;
};
let (position, yaw, width, attachment) = (
vehicle.position,
vehicle.yaw,
vehicle.width(),
riders::passenger_attachment(vehicle),
);
let mut children = Vec::new();
for m in self.mobs.iter_mut().filter(|m| m.extra.mount == Some(root)) {
m.position = riders::seat(
attachment,
riders::vehicle_attachment(m, width),
position,
yaw,
);
m.previous = m.position;
children.push(m.id);
}
for child in children {
self.snap_mob_passengers(child);
}
}
/// `Entity.teleportCrossDimension`: carry a command's entire vehicle tree
/// into the destination level, retaining UUIDs and the passengers' mounts.
pub(super) fn teleport_vehicle_across(
&mut self,
target: Target,
destination: super::Dim,
at: [f64; 3],
rotation: Option<[f32; 2]>,
turn_riders: bool,
) -> horizon_client::Result<bool> {
let Some(root) = vehicle_id(target) else {
return Ok(false);
};
self.in_target_level(target, |game| {
let source = game.dim;
if source == destination {
return Ok(false);
}
// Open the destination before changing or removing any entity.
game.enter_level(None, destination)?;
game.enter_level(None, source)?;
if game
.mobs
.iter()
.any(|m| m.id == root && m.extra.mount.is_some())
{
game.dismount_mob(root);
game.snap_mob_passengers(root);
}
game.prepare_vehicle_transition(root);
game.teleport_to_in_level(target, at, rotation, turn_riders);
let mounts: Vec<_> = game.mobs.iter().map(|m| (m.id, m.extra.mount)).collect();
let tree = super::persist::vehicle_tree(root, &mounts);
let tag = game.vehicle_tree_nbt(root).ok_or("missing vehicle tree")?;
let kind = horizon_client::entity_nbt::kind(&tag).ok_or("missing vehicle type")?;
let uuid = horizon_client::entity_nbt::uuid(&tag);
let mut players = Vec::new();
// A command runs with its source player swapped out of the hub.
if game.riding.is_some_and(|id| tree.contains(&id))
&& let (Some(conn), Some(player)) = (game.lan_acting, game.player.as_ref())
{
players.push((
conn,
[player.position.x, player.position.y, player.position.z],
[game.yaw, game.pitch],
game.riding.unwrap(),
));
}
if let Some(host) = game.lan.as_ref() {
for guest in &host.hub.guests {
if Some(guest.conn) != game.lan_acting
&& guest.vehicle.is_some_and(|id| tree.contains(&id))
{
players.push((
guest.conn,
guest.position,
[guest.yaw, guest.pitch],
guest.vehicle.unwrap(),
));
}
}
}
let players: Vec<_> = players
.into_iter()
.map(|(conn, at, rotation, mount)| (conn, at, rotation, game.uuid_for(mount)))
.collect();
game.mobs.retain(|m| !tree.contains(&m.id));
game.boats.retain(|b| !tree.contains(&b.id));
game.carts.retain(|c| !tree.contains(&c.id));
for id in tree {
game.release_vehicle_container(id);
game.detach_entity(id);
}
game.native_leaves_flush();
game.enter_level(None, destination)?;
// restoreFrom reloads saved state into fresh entities, rather than
// retaining navigation paths or goals from the old level.
game.load_listed(&kind, uuid, game.level.player_uuid, tag)
.map_err(|_| "could not restore vehicle tree")?;
// AbstractHorse.addPassenger snaps a reattached passenger to the
// new horse's getViewYRot(0) / getViewXRot(0). A fresh living
// entity's previous head yaw is still zero, before its first tick.
let new_root = uuid
.and_then(|uuid| game.vehicle_by_uuid(uuid))
.ok_or("restored vehicle is missing")?;
let mounts: Vec<_> = game.mobs.iter().map(|m| (m.id, m.extra.mount)).collect();
let restored = super::persist::vehicle_tree(new_root, &mounts);
let horses: Vec<_> = game
.mobs
.iter()
.filter(|m| restored.contains(&m.id) && is_horse(m.species()))
.map(|m| (m.id, m.head_pitch))
.collect();
for (horse, pitch) in horses {
for rider in game
.mobs
.iter_mut()
.filter(|m| m.extra.mount == Some(horse))
{
rider.yaw = 0.0;
rider.previous_yaw = 0.0;
rider.head_pitch = pitch;
rider.previous_head_pitch = pitch;
}
}
for (conn, at, rotation, mount) in players {
game.level_teleports
.push(super::server_travel::LevelTeleport {
conn,
dim: destination,
at,
rotation: Some(rotation),
vehicle: game.vehicle_by_uuid(mount),
});
}
game.native_joins_flush(false, 0);
game.enter_level(None, source)?;
Ok(true)
})
.unwrap_or(Ok(false))
}
/// `Entity.teleportSameDimension`: after `t` moved from `from` to `to`, its
/// riders move by the same offset, and to the angles the vehicle was
/// placed at when it turned (`calculatePassengerTransition`), staying
@@ -120,6 +409,7 @@ impl Game<'_> {
&& let Some(p) = self.player.as_mut()
{
p.position = passenger_destination(old, new, p.position);
p.teleported = true;
self.previous_position = p.position;
}
if let Some(placed) = rotation
@@ -176,7 +466,7 @@ impl Game<'_> {
/// A vehicle's yaw and pitch, for the riders' teleport packet.
fn vehicle_rotation(&self, id: u32) -> [f32; 2] {
if let Some(b) = self.boats.iter().find(|b| b.id == id) {
[b.yaw, 0.0]
[b.yaw, b.pitch]
} else if let Some(c) = self.carts.iter().find(|c| c.id == id) {
[c.yaw, c.pitch]
} else if let Some(m) = self.mobs.iter().find(|m| m.id == id) {
@@ -241,11 +531,17 @@ impl Game<'_> {
Target::Boat(id) if yaw.is_finite() => {
if let Some(b) = self.boats.iter_mut().find(|b| b.id == id) {
b.yaw = yaw;
if let Some(pitch) = pitch {
b.pitch = pitch;
}
}
}
Target::Cart(id) if yaw.is_finite() => {
if let Some(c) = self.carts.iter_mut().find(|c| c.id == id) {
c.yaw = yaw;
if let Some(pitch) = pitch {
c.pitch = pitch;
}
}
}
Target::Fangs(id) if yaw.is_finite() => {
+3
View File
@@ -4714,8 +4714,10 @@ def advancement_reward_functions():
def groups():
try:
from .loaded_cloud_corpus import loaded_clouds
from .passenger_teleport_corpus import passenger_teleports
except ImportError:
from loaded_cloud_corpus import loaded_clouds
from passenger_teleport_corpus import passenger_teleports
return [
('setup', group_setup()),
('bell server glow', bell_server_glow()),
@@ -4764,6 +4766,7 @@ def groups():
('sulfur cube fuse ticks', sulfur_cube_fuse_ticks()),
('wither spawn', wither_spawn()),
('riding teleport', riding_teleport()),
('passenger teleports', passenger_teleports(cmd, setup)),
('mob riding', mob_riding()),
('trial spawner passengers', trial_spawner_passengers()),
('camel riders', camel_riders()),
+51
View File
@@ -0,0 +1,51 @@
"""Vehicle command transitions compared against vanilla's integrated server."""
def passenger_teleports(cmd, setup):
"""#9335: nested passengers keep their offsets, turns and destination level."""
vehicle = '@e[tag=tp_tree_vehicle,limit=1]'
rider = '@e[tag=tp_tree_rider,limit=1]'
child = '@e[tag=tp_tree_child,limit=1]'
ops = setup('/forceload add 0 0', '/execute in minecraft:the_nether run tp @s 8.5 230 8.5')
ops.append({'op': 'settle'})
ops += setup('/forceload add 0 0', '/execute in minecraft:overworld run tp @s 8.5 230 8.5')
ops.append({'op': 'settle'})
for kind in ('horse', 'oak_boat', 'minecart'):
ops += setup('/kill @e[type=!player]',
f'/summon {kind} 2.5 210 2.5 '
'{NoAI:1b,NoGravity:1b,Tags:["tp_tree_vehicle"],'
'Passengers:[{id:"minecraft:pig",NoAI:1b,NoGravity:1b,Tags:["tp_tree_rider"],'
'Passengers:[{id:"minecraft:chicken",NoAI:1b,NoGravity:1b,Tags:["tp_tree_child"]}]}]}',
f'/rotate {vehicle} 10 20', f'/rotate {rider} 40 -30', f'/rotate {child} -20 50')
for destination in ('8 215 8 90 80', '~1 ~1 ~1 ~90 ~100', '3 211 3'):
ops.append(cmd(f'/tp {vehicle} {destination}'))
for entity in (vehicle, rider, child):
for field in ('Pos', 'Rotation'):
ops.append(cmd(f'/data get entity {entity} {field}', expect_result=3 if field == 'Pos' else 2))
for dimension in ('the_nether', 'overworld'):
ops.append(cmd(f'/execute in minecraft:{dimension} run tp {vehicle} 5 215 5 30 40'))
for entity in (vehicle, rider, child):
for field in ('Pos', 'Rotation'):
ops.append(cmd(f'/execute in minecraft:{dimension} run data get entity {entity} {field}',
expect_result=3 if field == 'Pos' else 2))
for entity in (rider, child):
ops.append(cmd(f'/execute in minecraft:{dimension} as {entity} on vehicle run data get entity @s Pos', expect_result=3))
other = 'overworld' if dimension == 'the_nether' else 'the_nether'
# A bounded selector pins which level holds each entity: no ghosts
# left behind and no missing descendants in the destination.
for entity in (vehicle, rider, child):
bounded = entity.replace('limit=1', 'limit=1,distance=..10000')
ops.append(cmd(f'/execute in minecraft:{other} positioned 0 210 0 unless entity {bounded}', expect_result=1))
ops += setup('/kill @e[type=!player]')
ops += setup('/summon oak_boat 4.5 210 4.5 {NoGravity:1b,Tags:["tp_tree_vehicle"]}')
ops.append(cmd(f'/ride @s mount {vehicle}', expect_result=1))
ops.append(cmd(f'/execute in minecraft:the_nether run tp {vehicle} 6 215 6'))
ops.append({'op': 'settle'})
ops.append(cmd('/execute if dimension minecraft:the_nether', expect_result=1))
ops.append(cmd('/execute on vehicle run data get entity @s Pos', expect_result=3))
ops.append(cmd('/data get entity @s Pos', expect_result=3))
ops += setup('/ride @s dismount', '/execute in minecraft:overworld run tp @s 8.5 230 8.5')
ops.append({'op': 'settle'})
ops += setup('/execute in minecraft:the_nether run kill @e[tag=tp_tree_vehicle]')
return ops