mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Carry vehicle passengers through command dimension teleports
Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
1989b1b595
commit
8f5f91f2f0
10 files changed
+438
-25
No files matched your search
@@ -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)),
|
||||
|
||||
@@ -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() => {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user