diff --git a/crates/horizon-client/src/game/commands.rs b/crates/horizon-client/src/game/commands.rs index bd3e5a721e..03021cb3c4 100644 --- a/crates/horizon-client/src/game/commands.rs +++ b/crates/horizon-client/src/game/commands.rs @@ -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 { + 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( diff --git a/crates/horizon-client/src/game/commands/execute.rs b/crates/horizon-client/src/game/commands/execute.rs index 3eac23d1bd..78f7d27cf9 100644 --- a/crates/horizon-client/src/game/commands/execute.rs +++ b/crates/horizon-client/src/game/commands/execute.rs @@ -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 diff --git a/crates/horizon-client/src/game/commands/world_cmds.rs b/crates/horizon-client/src/game/commands/world_cmds.rs index d4d30fd0da..ba301c38a1 100644 --- a/crates/horizon-client/src/game/commands/world_cmds.rs +++ b/crates/horizon-client/src/game/commands/world_cmds.rs @@ -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) diff --git a/crates/horizon-client/src/game/lan_actions.rs b/crates/horizon-client/src/game/lan_actions.rs index 320107fb45..796c78ef50 100644 --- a/crates/horizon-client/src/game/lan_actions.rs +++ b/crates/horizon-client/src/game/lan_actions.rs @@ -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; } diff --git a/crates/horizon-client/src/game/minecarts.rs b/crates/horizon-client/src/game/minecarts.rs index 90609d19b6..c919c15363 100644 --- a/crates/horizon-client/src/game/minecarts.rs +++ b/crates/horizon-client/src/game/minecarts.rs @@ -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)), diff --git a/crates/horizon-client/src/game/persist.rs b/crates/horizon-client/src/game/persist.rs index 4e14932dff..044482471c 100644 --- a/crates/horizon-client/src/game/persist.rs +++ b/crates/horizon-client/src/game/persist.rs @@ -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 { + pub(super) fn vehicle_tree_nbt(&mut self, root: u32) -> Option { 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)]) -> Vec { +pub(super) fn vehicle_tree(root: u32, mounts: &[(u32, Option)]) -> Vec { let mut tree = vec![root]; let mut k = 0; while k < tree.len() { diff --git a/crates/horizon-client/src/game/server_travel.rs b/crates/horizon-client/src/game/server_travel.rs index f848b7ce21..4eda952701 100644 --- a/crates/horizon-client/src/game/server_travel.rs +++ b/crates/horizon-client/src/game/server_travel.rs @@ -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, } /// 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 diff --git a/crates/horizon-client/src/game/vehicle_teleport.rs b/crates/horizon-client/src/game/vehicle_teleport.rs index 1691e82548..be646e3773 100644 --- a/crates/horizon-client/src/game/vehicle_teleport.rs +++ b/crates/horizon-client/src/game/vehicle_teleport.rs @@ -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 { } impl Game<'_> { + /// `AbstractMinecart.getDismountLocationForPassenger` for a standing mob. + pub(super) fn cart_dismount_point(&self, vehicle: u32, rider: usize) -> Option { + 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 { + 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 { + 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() => { diff --git a/tools/command_exec_corpus.py b/tools/command_exec_corpus.py index b65f300d5e..0518f99aac 100644 --- a/tools/command_exec_corpus.py +++ b/tools/command_exec_corpus.py @@ -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()), diff --git a/tools/passenger_teleport_corpus.py b/tools/passenger_teleport_corpus.py new file mode 100644 index 0000000000..cbe326f1bc --- /dev/null +++ b/tools/passenger_teleport_corpus.py @@ -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 +