mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 08:07:53 -10:00
Merge origin/main into feat/vanilla-menus
With #1285 Play with Friends works on the server thread: friends_view offers sharing there, so the pause menu's Friends icon is active on both paths and inactive only on another server. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
bf274d9254
18 files changed
+957
-86
No files matched your search
@@ -75,6 +75,7 @@ mod lan_pvp;
|
||||
mod lan_screen;
|
||||
mod lan_stations;
|
||||
mod lan_trading;
|
||||
mod lan_vehicles;
|
||||
mod lan_waypoints;
|
||||
mod leads;
|
||||
mod level_particles;
|
||||
@@ -120,6 +121,7 @@ mod remote;
|
||||
mod remote_objects;
|
||||
mod remote_projectiles;
|
||||
mod remote_stations;
|
||||
mod remote_vehicles;
|
||||
mod render_control;
|
||||
mod render_tick;
|
||||
mod respawn_anchor;
|
||||
@@ -1321,6 +1323,8 @@ struct Game<'a> {
|
||||
/// `ClientAvatarState`: walked distance and view bob.
|
||||
avatar: horizon_client::camera::AvatarBob,
|
||||
flash: u32,
|
||||
/// Bolts that struck here and sky flashes they made, in total (`status`).
|
||||
lightning_seen: [u32; 2],
|
||||
/// The head of the bed while sleeping; the visual timer also survives waking.
|
||||
sleeping: Option<[i32; 3]>,
|
||||
sleep_timer: u32,
|
||||
@@ -11554,6 +11558,7 @@ impl<'a> Game<'a> {
|
||||
dragging: false,
|
||||
avatar: Default::default(),
|
||||
flash: 0,
|
||||
lightning_seen: [0; 2],
|
||||
sleeping: None,
|
||||
sleep_timer: 0,
|
||||
dim: Dim::Overworld,
|
||||
|
||||
@@ -57,7 +57,7 @@ pub(super) const CHEST_SLOTS: usize = 27;
|
||||
const WIDTH: f64 = 1.375;
|
||||
const HEIGHT: f64 = 0.5625;
|
||||
/// Player.DEFAULT_VEHICLE_ATTACHMENT minus the boat's passenger attachment.
|
||||
const SEAT: f64 = 0.1875 - 0.6;
|
||||
pub(super) const SEAT: f64 = 0.1875 - 0.6;
|
||||
impl Boat {
|
||||
pub fn new(id: u32, wood: Arc<str>, position: Vec3, yaw: f32) -> Self {
|
||||
Self {
|
||||
@@ -295,7 +295,7 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
/// `AbstractBoat.tick` for one boat (`controlled` when the player rides it).
|
||||
fn tick_boat(&mut self, mut b: Boat, controlled: bool) -> Boat {
|
||||
pub(super) fn tick_boat(&mut self, mut b: Boat, controlled: bool) -> Boat {
|
||||
b.previous = b.position;
|
||||
b.previous_yaw = b.yaw;
|
||||
b.old_status = b.status;
|
||||
@@ -448,9 +448,12 @@ impl Game<'_> {
|
||||
pub(super) fn tick_boats(&mut self) {
|
||||
// ServerLevel.tick: only entities in entity-ticking chunks tick.
|
||||
let ticking = self.entity_ticking_area();
|
||||
// A server player's boat is driven by its client (lan_vehicles.rs).
|
||||
let driven = self.guest_vehicles();
|
||||
for i in 0..self.boats.len() {
|
||||
let p = self.boats[i].position;
|
||||
if Some(self.boats[i].id) == self.riding
|
||||
|| driven.contains(&self.boats[i].id)
|
||||
|| !ticking(p.x.floor() as i32, p.z.floor() as i32)
|
||||
{
|
||||
continue;
|
||||
@@ -659,7 +662,11 @@ impl Game<'_> {
|
||||
self.right = false;
|
||||
return true;
|
||||
}
|
||||
if sneaking || self.boats[i].out_of_control >= 60.0 || self.riding.is_some() {
|
||||
if sneaking
|
||||
|| self.boats[i].out_of_control >= 60.0
|
||||
|| self.riding.is_some()
|
||||
|| self.ridden_by_guest(self.boats[i].id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.riding = Some(self.boats[i].id);
|
||||
|
||||
@@ -929,6 +929,8 @@ impl Game<'_> {
|
||||
status["heard"] = json!(self.audio.heard);
|
||||
// Boats, minecarts, armor stands, item frames and paintings.
|
||||
status["objects"] = json!(self.objects_status());
|
||||
status["lightning"] = json!({"bolts": self.bolts.len(), "struck": self.lightning_seen[0],
|
||||
"flashes": self.lightning_seen[1], "flash": self.flash});
|
||||
// Primed TNT, falling blocks, projectiles, tridents and end crystals.
|
||||
status["projectiles"] = json!(self.projectiles_status());
|
||||
status
|
||||
|
||||
@@ -58,10 +58,12 @@ impl Game<'_> {
|
||||
if self.friends.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
if self.remote.is_some() || self.title.is_some() {
|
||||
// Single player on its server thread shares that server, as Open to
|
||||
// LAN publishes it; another server can't be shared.
|
||||
if self.title.is_some() || (self.remote.is_some() && self.integrated.is_none()) {
|
||||
return Err("open a singleplayer world first".into());
|
||||
}
|
||||
let opened_lan = self.lan.is_none();
|
||||
let opened_lan = self.lan_port().is_none();
|
||||
if opened_lan {
|
||||
// ShareToLanScreen's defaults, as the world options screen offers them.
|
||||
self.lan_open(LanSettings {
|
||||
@@ -210,14 +212,20 @@ impl Game<'_> {
|
||||
|
||||
/// What Play with Friends shows.
|
||||
pub(super) fn friends_view(&self) -> FriendsView {
|
||||
let guests = self
|
||||
.lan
|
||||
.as_ref()
|
||||
.map(|h| h.hub.guests.iter().map(|g| g.name.clone()).collect())
|
||||
.unwrap_or_default();
|
||||
let guests = match &self.lan {
|
||||
Some(h) => h.hub.guests.iter().map(|g| g.name.clone()).collect(),
|
||||
// On the server thread: the other players the server lists.
|
||||
None if self.integrated.is_some() => {
|
||||
let me = self.options.player_name();
|
||||
let mut names = self.remote_online_names().unwrap_or_default();
|
||||
names.retain(|n| *n != me);
|
||||
names
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
let Some(s) = &self.friends else {
|
||||
return FriendsView {
|
||||
shown: self.remote.is_none(),
|
||||
shown: self.remote.is_none() || self.integrated.is_some(),
|
||||
..FriendsView::default()
|
||||
};
|
||||
};
|
||||
|
||||
@@ -807,7 +807,13 @@ impl Game<'_> {
|
||||
if has_guests {
|
||||
let host_entity = state.as_ref().map(|s| s.entity_id);
|
||||
self.lan_assign_ids();
|
||||
self.lan_riding(host_entity, &mut host.hub.riding);
|
||||
let guests: Vec<(u32, i32)> = host
|
||||
.hub
|
||||
.guests
|
||||
.iter()
|
||||
.filter_map(|g| Some((g.vehicle?, g.entity_id)))
|
||||
.collect();
|
||||
self.lan_riding(host_entity, &guests, &mut host.hub.riding);
|
||||
self.lan_entities(host_entity, &mut host.hub, &mut states);
|
||||
} else {
|
||||
states.clear();
|
||||
|
||||
@@ -292,7 +292,8 @@ impl Game<'_> {
|
||||
let (left, right) = (self.left, self.right);
|
||||
self.left = play.digging.is_some();
|
||||
self.right = play.using.is_some() || self.using.is_some();
|
||||
let riding = self.riding.take();
|
||||
// The guest's vehicle (Entity.vehicle) for its turn.
|
||||
let riding = std::mem::replace(&mut self.riding, g.vehicle);
|
||||
let host_station = self.station.take();
|
||||
// The guest's bed, sleep counter and respawn point; the host's
|
||||
// respawn point, anchor and action bar stay theirs.
|
||||
@@ -467,7 +468,7 @@ impl Game<'_> {
|
||||
(self.respawn_config, self.level.spawn) = respawn;
|
||||
// ServerPlayer.displayClientMessage(_, true): the guest's action bar.
|
||||
let guest_message = std::mem::replace(&mut self.message, message);
|
||||
self.riding = riding;
|
||||
let guest_vehicle = std::mem::replace(&mut self.riding, riding);
|
||||
self.left = left;
|
||||
self.right = right;
|
||||
play.mining = std::mem::replace(&mut self.mining, mining);
|
||||
@@ -491,6 +492,11 @@ impl Game<'_> {
|
||||
self.mount_menu = host_mount_menu;
|
||||
self.keys = host_keys;
|
||||
let g = &mut host.hub.guests[i];
|
||||
let left_vehicle = g.vehicle.filter(|_| guest_vehicle.is_none());
|
||||
if guest_vehicle.is_none() {
|
||||
g.vehicle_good = None;
|
||||
}
|
||||
g.vehicle = guest_vehicle;
|
||||
let fell_asleep = bed.is_some() && g.bed.is_none();
|
||||
(g.bed, g.sleep_timer, g.spawn) = (bed, guest_sleep_timer, guest_spawn);
|
||||
g.sleeping = g.bed.is_some();
|
||||
@@ -549,7 +555,24 @@ impl Game<'_> {
|
||||
let teleported = std::mem::take(&mut m.teleported);
|
||||
let moved: f64 = (0..3).map(|k| (at[k] - g.position[k]).powi(2)).sum();
|
||||
g.motion = Some(Box::new(m));
|
||||
if moved > 0.0001 || turned || teleported {
|
||||
// Entity.teleportTo takes a rider out of its vehicle.
|
||||
let left_vehicle = match g.vehicle.filter(|_| teleported) {
|
||||
Some(v) => {
|
||||
(g.vehicle, g.vehicle_good) = (None, None);
|
||||
Some(v)
|
||||
}
|
||||
None => left_vehicle,
|
||||
};
|
||||
// ServerPlayer.dismountTo: the client hears it is out of the
|
||||
// vehicle before the teleport beside it. A rider sits where its
|
||||
// vehicle puts it (lan_vehicles.rs), with no teleport.
|
||||
if let Some(vehicle) = left_vehicle {
|
||||
g.send(&C::SetPassengers(play::SetPassengers {
|
||||
vehicle: vehicle as i32,
|
||||
passengers: Vec::new(),
|
||||
}));
|
||||
}
|
||||
if g.vehicle.is_none() && (moved > 0.0001 || turned || teleported) {
|
||||
host.hub.teleport(i, Some(at));
|
||||
}
|
||||
}
|
||||
@@ -900,16 +923,22 @@ impl Game<'_> {
|
||||
}
|
||||
S::Attack(p) => self.guest_attack(host, conn, p.id),
|
||||
S::Interact(p) => {
|
||||
if let Some((position, eye)) = self.look_at_entity(host, gi, p.id) {
|
||||
let _ = (position, eye);
|
||||
let before = host.hub.guests[gi].vehicle;
|
||||
if self
|
||||
.look_at_entity(host, gi, p.id, Some(p.location))
|
||||
.is_some()
|
||||
{
|
||||
let swung = self.as_guest(host, conn, |g| {
|
||||
let swings = g.swings_started;
|
||||
let _ = g.use_item();
|
||||
g.swings_started != swings
|
||||
});
|
||||
self.lan_swing_back(host, conn, swung);
|
||||
self.guest_boarded(host, conn, before);
|
||||
}
|
||||
}
|
||||
S::MoveVehicle(p) => self.guest_move_vehicle(host, conn, &p),
|
||||
S::PaddleBoat(p) => self.guest_paddle_boat(host, conn, [p.left, p.right]),
|
||||
S::SetCreativeModeSlot(p) => {
|
||||
let g = &mut host.hub.guests[gi];
|
||||
if !g.creative() || !(0..46).contains(&p.slot) {
|
||||
@@ -1147,19 +1176,39 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Aim the swapped-in player at an entity; returns its centre and the eye.
|
||||
/// Aim the swapped-in player at an entity; returns where it aims and the
|
||||
/// eye. A boat, minecart, armor stand, item frame or painting is aimed at
|
||||
/// the point the client hit (the interact packet's location) when that
|
||||
/// is on it, else at its centre.
|
||||
fn look_at_entity(
|
||||
&mut self,
|
||||
host: &mut LanHost,
|
||||
gi: usize,
|
||||
id: i32,
|
||||
location: Option<[f64; 3]>,
|
||||
) -> Option<([f64; 3], [f64; 3])> {
|
||||
let object = || {
|
||||
let (lo, hi, origin) = self.object_target(id)?;
|
||||
let middle = [0, 1, 2].map(|k| (lo[k] + hi[k]) / 2.0);
|
||||
let Some(l) = location else {
|
||||
return Some(middle);
|
||||
};
|
||||
let point = [0, 1, 2].map(|k| origin[k] + l[k]);
|
||||
// A little inside the box, so the ray from the eye meets it.
|
||||
let inside = [0, 1, 2].map(|k| {
|
||||
let margin = ((hi[k] - lo[k]) / 2.0).min(0.01);
|
||||
point[k].clamp(lo[k] + margin, hi[k] - margin)
|
||||
});
|
||||
let on = (0..3).all(|k| (point[k] - inside[k]).abs() < 0.1);
|
||||
Some(if on { inside } else { middle })
|
||||
};
|
||||
let centre = match self.mobs.iter().find(|m| m.id as i32 == id) {
|
||||
Some(m) => [
|
||||
m.position.x,
|
||||
m.position.y + m.kind.height * 0.5,
|
||||
m.position.z,
|
||||
],
|
||||
None if self.object_target(id).is_some() => object()?,
|
||||
// A dragon part or an end crystal (game/dragon.rs).
|
||||
None => {
|
||||
let b = self.tracked_special_box(id)?;
|
||||
@@ -1210,7 +1259,7 @@ impl Game<'_> {
|
||||
return;
|
||||
}
|
||||
let (old_yaw, old_pitch) = (host.hub.guests[gi].yaw, host.hub.guests[gi].pitch);
|
||||
if self.look_at_entity(host, gi, id).is_none() {
|
||||
if self.look_at_entity(host, gi, id, None).is_none() {
|
||||
return;
|
||||
}
|
||||
self.as_guest(host, conn, |g| {
|
||||
@@ -1676,6 +1725,7 @@ impl Game<'_> {
|
||||
/// regeneration and the food tick (ServerPlayer.doTick). Their hurt
|
||||
/// sounds are heard at the guest.
|
||||
fn guest_server_tick(&mut self, host: &mut LanHost, conn: u32) {
|
||||
self.guest_vehicle_tick(host, conn);
|
||||
let Some(g) = host.hub.guest_mut(conn) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -89,8 +89,17 @@ fn stand_poses(raw: Option<&Tag>) -> Vec<(u8, i32, Value)> {
|
||||
impl Game<'_> {
|
||||
/// Who rides what (`Entity.getPassengers`): mobs on mobs, and the
|
||||
/// host in its boat or minecart.
|
||||
pub(super) fn lan_riding(&self, host_entity: Option<i32>, riding: &mut Riding) {
|
||||
pub(super) fn lan_riding(
|
||||
&self,
|
||||
host_entity: Option<i32>,
|
||||
guests: &[(u32, i32)],
|
||||
riding: &mut Riding,
|
||||
) {
|
||||
riding.clear();
|
||||
// The server's players in their boats and minecarts.
|
||||
for &(vehicle, guest) in guests {
|
||||
riding.add(vehicle as i32, guest);
|
||||
}
|
||||
for m in self.mobs.iter().filter(|m| !m.removed) {
|
||||
if let Some(vehicle) = m.extra.mount {
|
||||
riding.add(vehicle as i32, m.id as i32);
|
||||
@@ -264,6 +273,7 @@ impl Game<'_> {
|
||||
self.tridents.iter_mut().for_each(|t| give(&mut t.id));
|
||||
self.rockets.iter_mut().for_each(|r| give(&mut r.id));
|
||||
self.crystals.iter_mut().for_each(|c| give(&mut c.id));
|
||||
self.bolts.iter_mut().for_each(|b| give(&mut b.id));
|
||||
}
|
||||
|
||||
/// Primed TNT, falling blocks, projectiles, tridents, firework rockets and
|
||||
@@ -275,6 +285,10 @@ impl Game<'_> {
|
||||
None => 0,
|
||||
};
|
||||
let at = |p: horizon_core::geometry::Vec3| [p.x, p.y, p.z];
|
||||
// LightningBolt: each client draws and flashes its own.
|
||||
for b in self.bolts.iter().filter(|b| b.id != 0) {
|
||||
out.push(state(b.id, "minecraft:lightning_bolt", b.at, 0.0, false));
|
||||
}
|
||||
for t in self.tnts.iter().filter(|t| t.id != 0) {
|
||||
let mut e = state(t.id, "minecraft:tnt", at(t.position), 0.0, t.on_ground);
|
||||
e.velocity = at(t.velocity);
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
//! Boats and minecarts for a server's players: LAN guests, players on a
|
||||
//! dedicated server, and single player's window on its server thread.
|
||||
//!
|
||||
//! A player aims at a vehicle, item frame, armor stand or painting as at a
|
||||
//! mob (`ServerGamePacketListenerImpl.handleInteract`), and the game's own
|
||||
//! code for it runs as that player (`AbstractBoat.interact` seats it,
|
||||
//! `VehicleEntity.hurtServer` breaks the vehicle). A boat is driven by its
|
||||
//! rider's client (`handleMoveVehicle`, `handlePaddleBoat`); a minecart moves
|
||||
//! here with the rider's input (`ServerPlayer.getLastClientMoveIntent`).
|
||||
//! Sneaking gets out (`Player.rideTick`: `wantsToStopRiding`), and the client
|
||||
//! is teleported where the player lands (`ServerPlayer.dismountTo`).
|
||||
use super::{Game, Keys, boats::SEAT, lan::LanHost};
|
||||
use horizon_client::collisions::SceneCollisions;
|
||||
use horizon_core::geometry::{Aabb, Vec3};
|
||||
use horizon_protocol::{
|
||||
packets::play::{self, Clientbound as C},
|
||||
text::translatable,
|
||||
};
|
||||
|
||||
/// `PlayerInput` flags, as the server player's client last sent them.
|
||||
pub(super) fn input_keys(flags: u8) -> Keys {
|
||||
Keys {
|
||||
forward: flags & 1 != 0,
|
||||
backward: flags & 2 != 0,
|
||||
left: flags & 4 != 0,
|
||||
right: flags & 8 != 0,
|
||||
jump: flags & 16 != 0,
|
||||
shift: flags & 32 != 0,
|
||||
..Keys::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn corners(b: Aabb) -> ([f64; 3], [f64; 3]) {
|
||||
let (lo, hi) = (b.min_position(), b.max_position());
|
||||
([lo.x, lo.y, lo.z], [hi.x, hi.y, hi.z])
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// The box of a boat, minecart, armor stand, item frame or painting, and
|
||||
/// the point an interact packet's location is measured from (the
|
||||
/// client's `remote_object_hit` uses the same points).
|
||||
pub(super) fn object_target(&self, id: i32) -> Option<([f64; 3], [f64; 3], [f64; 3])> {
|
||||
let id = u32::try_from(id).ok()?;
|
||||
let at = |v: Vec3| [v.x, v.y, v.z];
|
||||
let centre = |p: [i32; 3]| p.map(|v| f64::from(v) + 0.5);
|
||||
if let Some(b) = self.boats.iter().find(|b| b.id == id) {
|
||||
let (lo, hi) = corners(b.bounds());
|
||||
return Some((lo, hi, at(b.position)));
|
||||
}
|
||||
if let Some(c) = self.carts.iter().find(|c| c.id == id) {
|
||||
let (lo, hi) = corners(c.bounds());
|
||||
return Some((lo, hi, at(c.position)));
|
||||
}
|
||||
if let Some(a) = self.armor_stands.iter().find(|a| a.id == id) {
|
||||
let (lo, hi) = corners(a.bounds());
|
||||
return Some((lo, hi, at(a.position)));
|
||||
}
|
||||
if let Some(f) = self.item_frames.iter().find(|f| f.id == id) {
|
||||
let (lo, hi) = f.bounds();
|
||||
return Some((lo, hi, centre(f.pos)));
|
||||
}
|
||||
let p = self.paintings.iter().find(|p| p.id == id)?;
|
||||
let (lo, hi) = p.bounds();
|
||||
Some((lo, hi, centre(p.pos)))
|
||||
}
|
||||
|
||||
/// The vehicles the server's players ride: they move with their riders,
|
||||
/// not in `tick_boats` and `tick_carts`.
|
||||
pub(super) fn guest_vehicles(&self) -> Vec<u32> {
|
||||
self.lan.as_ref().map_or_else(Vec::new, |h| {
|
||||
h.hub.guests.iter().filter_map(|g| g.vehicle).collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a server player other than the one acting rides `vehicle`
|
||||
/// (`Entity.canAddPassenger`: a boat or minecart seats one player here).
|
||||
pub(super) fn ridden_by_guest(&self, vehicle: u32) -> bool {
|
||||
self.lan.as_ref().is_some_and(|h| {
|
||||
h.hub
|
||||
.guests
|
||||
.iter()
|
||||
.any(|g| Some(g.conn) != self.lan_acting && g.vehicle == Some(vehicle))
|
||||
})
|
||||
}
|
||||
|
||||
/// `Player.rideTick` for a server player, before its moves this tick:
|
||||
/// it stays in its seat, steers its minecart, or gets out.
|
||||
pub(super) fn guest_vehicle_tick(&mut self, host: &mut LanHost, conn: u32) {
|
||||
let Some(gi) = host.hub.guests.iter().position(|g| g.conn == conn) else {
|
||||
return;
|
||||
};
|
||||
let g = &host.hub.guests[gi];
|
||||
let Some(vehicle) = g.vehicle else {
|
||||
return;
|
||||
};
|
||||
let (keys, dead) = (input_keys(g.input), g.dead || g.health <= 0.0);
|
||||
let boat = self.boats.iter().position(|b| b.id == vehicle);
|
||||
let cart = self.carts.iter().any(|c| c.id == vehicle);
|
||||
// The vehicle is gone (broken, in another level) or the rider died:
|
||||
// its client drops it with the entity or the death screen.
|
||||
if dead || (boat.is_none() && !cart) {
|
||||
let g = &mut host.hub.guests[gi];
|
||||
(g.vehicle, g.vehicle_good) = (None, None);
|
||||
return;
|
||||
}
|
||||
if let Some(i) = boat {
|
||||
// AbstractBoat.tick on the server for a boat its rider drives:
|
||||
// only the hurt animation and the damage wear off here.
|
||||
let b = &mut self.boats[i];
|
||||
b.hurt_time = (b.hurt_time - 1).max(0);
|
||||
b.damage = (b.damage - 1.0).max(0.0);
|
||||
}
|
||||
// Getting out goes through leave_guest, which tells the client and
|
||||
// teleports it (ServerPlayer.dismountTo).
|
||||
let seat = self.as_guest(host, conn, |game| {
|
||||
if let Some(i) = game.boats.iter().position(|b| Some(b.id) == game.riding) {
|
||||
if keys.shift {
|
||||
game.dismount(i);
|
||||
return None;
|
||||
}
|
||||
let p = game.boats[i].position;
|
||||
return Some([p.x, p.y + SEAT, p.z]);
|
||||
}
|
||||
// The minecart moves with the rider's input, and lets it out when
|
||||
// it sneaks or the cart breaks.
|
||||
game.tick_cart_riding(keys);
|
||||
game.riding?;
|
||||
let p = game.player.as_ref()?.position;
|
||||
Some([p.x, p.y, p.z])
|
||||
});
|
||||
if let Some(Some(at)) = seat {
|
||||
let g = &mut host.hub.guests[gi];
|
||||
g.previous_position = g.position;
|
||||
g.position = at;
|
||||
}
|
||||
}
|
||||
|
||||
/// After an interaction: a player that got into a vehicle sits in it,
|
||||
/// and its last good vehicle position is where the vehicle is.
|
||||
pub(super) fn guest_boarded(&mut self, host: &mut LanHost, conn: u32, before: Option<u32>) {
|
||||
let Some(gi) = host.hub.guests.iter().position(|g| g.conn == conn) else {
|
||||
return;
|
||||
};
|
||||
let Some(vehicle) = host.hub.guests[gi].vehicle else {
|
||||
return;
|
||||
};
|
||||
if before == Some(vehicle) {
|
||||
return;
|
||||
}
|
||||
// The host in the window rides it already.
|
||||
if self.riding == Some(vehicle) {
|
||||
host.hub.guests[gi].vehicle = before;
|
||||
return;
|
||||
}
|
||||
let at = self
|
||||
.boats
|
||||
.iter()
|
||||
.find(|b| b.id == vehicle)
|
||||
.map(|b| b.position)
|
||||
.or_else(|| {
|
||||
self.carts
|
||||
.iter()
|
||||
.find(|c| c.id == vehicle)
|
||||
.map(|c| c.position)
|
||||
});
|
||||
let g = &mut host.hub.guests[gi];
|
||||
if let Some(p) = at {
|
||||
g.vehicle_good = Some([p.x, p.y, p.z]);
|
||||
g.previous_position = g.position;
|
||||
g.position = [p.x, p.y + SEAT, p.z];
|
||||
g.on_ground = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// `ServerGamePacketListenerImpl.handleMoveVehicle`: the boat its rider
|
||||
/// drives goes where the client says, unless it moved too quickly or
|
||||
/// wrongly; then it stays, and the client is told where it is.
|
||||
pub(super) fn guest_move_vehicle(
|
||||
&mut self,
|
||||
host: &mut LanHost,
|
||||
conn: u32,
|
||||
p: &play::MoveVehicleRequest,
|
||||
) {
|
||||
let Some(gi) = host.hub.guests.iter().position(|g| g.conn == conn) else {
|
||||
return;
|
||||
};
|
||||
let [x, y, z] = p.position;
|
||||
if !(x.is_finite() && y.is_finite() && z.is_finite())
|
||||
|| !p.yaw.is_finite()
|
||||
|| !p.pitch.is_finite()
|
||||
{
|
||||
host.hub.kick(
|
||||
conn,
|
||||
translatable("multiplayer.disconnect.invalid_vehicle_movement", &[]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let g = &host.hub.guests[gi];
|
||||
if g.awaiting_teleport.is_some() || !g.loaded {
|
||||
return;
|
||||
}
|
||||
let owner = host.hub.singleplayer_owner == Some(g.uuid);
|
||||
let (vehicle, good) = (g.vehicle, g.vehicle_good);
|
||||
let back = |host: &mut LanHost, position: Vec3, yaw: f32| {
|
||||
let packet = play::MoveVehicle {
|
||||
position: [position.x, position.y, position.z],
|
||||
yaw,
|
||||
pitch: 0.0,
|
||||
};
|
||||
host.hub.send_to(conn, &C::MoveVehicle(packet));
|
||||
};
|
||||
// Only a boat is driven by its rider (getControllingPassenger); a
|
||||
// minecart that is elsewhere on the client is put back.
|
||||
let Some(i) = vehicle.and_then(|v| self.boats.iter().position(|b| b.id == v)) else {
|
||||
if let Some(c) = vehicle.and_then(|v| self.carts.iter().find(|c| c.id == v))
|
||||
&& c.position.distance_squared(Vec3::new(x, y, z)) > 1.0
|
||||
{
|
||||
back(host, c.position, c.yaw);
|
||||
}
|
||||
return;
|
||||
};
|
||||
let b = &self.boats[i];
|
||||
let old = b.position;
|
||||
let good = good.map_or(old, |g| Vec3::new(g[0], g[1], g[2]));
|
||||
let target = Vec3::new(
|
||||
x.clamp(-3.0E7, 3.0E7),
|
||||
y.clamp(-2.0E7, 2.0E7),
|
||||
z.clamp(-3.0E7, 3.0E7),
|
||||
);
|
||||
// Moved too quickly: more than 10 blocks past its own motion.
|
||||
let moved = target.distance_squared(old) - b.velocity.length_squared();
|
||||
if moved > 100.0 && !owner {
|
||||
log::warn!("boat {} moved too quickly! {moved}", b.id);
|
||||
back(host, old, b.yaw);
|
||||
return;
|
||||
}
|
||||
// vehicle.move(PLAYER, target - lastGood) with the boat's collisions:
|
||||
// where it can't follow the client (a quarter block off, the height
|
||||
// aside), it moved wrongly.
|
||||
let bounds = b.bounds();
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
let wanted = target.subtract(good);
|
||||
let resolved = scene
|
||||
.resolve(bounds, wanted, 0.0, b.on_ground)
|
||||
.unwrap_or(wanted);
|
||||
let reached = old.add_vector(resolved);
|
||||
let wrongly = (target.x - reached.x).powi(2) + (target.z - reached.z).powi(2) > 0.0625;
|
||||
let free_before = scene.unobstructed(bounds).unwrap_or(true);
|
||||
// isEntityCollidingWithAnythingNew: a box that was free is not there.
|
||||
let moved_box = bounds.move_by(target.subtract(old));
|
||||
let free_after = scene
|
||||
.unobstructed(moved_box.deflate(Vec3::new(1.0E-5, 1.0E-5, 1.0E-5)))
|
||||
.unwrap_or(true);
|
||||
if free_before && (wrongly || !free_after) {
|
||||
log::warn!("boat {} moved wrongly", b.id);
|
||||
back(host, old, b.yaw);
|
||||
return;
|
||||
}
|
||||
let b = &mut self.boats[i];
|
||||
b.previous = old;
|
||||
b.velocity = target.subtract(old);
|
||||
b.position = target;
|
||||
b.previous_yaw = b.yaw;
|
||||
b.yaw = p.yaw;
|
||||
b.on_ground = p.on_ground;
|
||||
let g = &mut host.hub.guests[gi];
|
||||
g.vehicle_good = Some([target.x, target.y, target.z]);
|
||||
g.previous_position = g.position;
|
||||
g.position = [target.x, target.y + SEAT, target.z];
|
||||
}
|
||||
|
||||
/// `handlePaddleBoat`: the paddles of the boat the player drives.
|
||||
pub(super) fn guest_paddle_boat(&mut self, host: &LanHost, conn: u32, paddles: [bool; 2]) {
|
||||
let vehicle = host
|
||||
.hub
|
||||
.guests
|
||||
.iter()
|
||||
.find(|g| g.conn == conn)
|
||||
.and_then(|g| g.vehicle);
|
||||
if let Some(b) = self.boats.iter_mut().find(|b| Some(b.id) == vehicle) {
|
||||
b.paddling = paddles;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ use horizon_client::mobs::{Damage, DamageKind, MobEvent, SpawnSetup, Species};
|
||||
use horizon_core::{position::BlockPos, random::RandomSource};
|
||||
|
||||
pub(super) struct Bolt {
|
||||
at: [f64; 3],
|
||||
/// The entity id a server's players know it by (0 until it has one).
|
||||
pub id: u32,
|
||||
pub at: [f64; 3],
|
||||
life: i32,
|
||||
flashes: i32,
|
||||
seed: i64,
|
||||
@@ -131,7 +133,9 @@ impl Game<'_> {
|
||||
let r = &mut self.world.random;
|
||||
let seed = r.next_long();
|
||||
let flashes = r.next_int_bound(3).unwrap_or(0) + 1;
|
||||
self.lightning_seen[0] += 1;
|
||||
self.bolts.push(Bolt {
|
||||
id: 0,
|
||||
at,
|
||||
life: 2,
|
||||
flashes,
|
||||
@@ -140,6 +144,24 @@ impl Game<'_> {
|
||||
converted: Vec::new(),
|
||||
});
|
||||
}
|
||||
/// `ClientLevel.addEntity` for a server's lightning bolt: on a client it
|
||||
/// only flashes, sounds and draws (`LightningBolt.tick` off the server).
|
||||
pub(super) fn spawn_remote_bolt(&mut self, id: u32, at: [f64; 3]) {
|
||||
self.bolts.retain(|b| b.id != id);
|
||||
let r = &mut self.world.random;
|
||||
let seed = r.next_long();
|
||||
let flashes = r.next_int_bound(3).unwrap_or(0) + 1;
|
||||
self.lightning_seen[0] += 1;
|
||||
self.bolts.push(Bolt {
|
||||
id,
|
||||
at,
|
||||
life: 2,
|
||||
flashes,
|
||||
seed,
|
||||
visual_only: true,
|
||||
converted: Vec::new(),
|
||||
});
|
||||
}
|
||||
/// `spawnFire`: fire at the strike point and `extra` random neighbours.
|
||||
fn bolt_fire(&mut self, at: [f64; 3], extra: i32) {
|
||||
if self.ui.difficulty < 2 {
|
||||
@@ -216,6 +238,9 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
if b.life >= 0 {
|
||||
if self.flash == 0 {
|
||||
self.lightning_seen[1] += 1;
|
||||
}
|
||||
self.flash = 2;
|
||||
if !b.visual_only {
|
||||
self.thunder_hit(b);
|
||||
|
||||
@@ -775,10 +775,13 @@ impl Game<'_> {
|
||||
self.push_carts();
|
||||
// ServerLevel.tick: only entities in entity-ticking chunks tick.
|
||||
let ticking = self.entity_ticking_area();
|
||||
// A server player's minecart moves with its rider (lan_vehicles.rs).
|
||||
let ridden = self.guest_vehicles();
|
||||
let mut i = 0;
|
||||
while i < self.carts.len() {
|
||||
let p = self.carts[i].position;
|
||||
if Some(self.carts[i].id) == self.riding
|
||||
|| ridden.contains(&self.carts[i].id)
|
||||
|| !ticking(p.x.floor() as i32, p.z.floor() as i32)
|
||||
{
|
||||
i += 1;
|
||||
@@ -986,7 +989,7 @@ impl Game<'_> {
|
||||
let held = self.held().cloned();
|
||||
match self.carts[i].kind {
|
||||
CartKind::Rideable => {
|
||||
if sneaking || self.riding.is_some() {
|
||||
if sneaking || self.riding.is_some() || self.ridden_by_guest(self.carts[i].id) {
|
||||
return false;
|
||||
}
|
||||
self.riding = Some(self.carts[i].id);
|
||||
|
||||
@@ -93,7 +93,7 @@ pub(super) struct Remote {
|
||||
pub(super) registries: Arc<Dynamic>,
|
||||
joined: bool,
|
||||
status: String,
|
||||
entity_id: i32,
|
||||
pub(super) entity_id: i32,
|
||||
dimension: DimensionType,
|
||||
biome_map: Vec<u16>,
|
||||
biome_bits: u8,
|
||||
@@ -1367,6 +1367,10 @@ impl Game<'_> {
|
||||
self.turn_remote_projectile(id, rotation);
|
||||
}
|
||||
fn remove_entity(&mut self, id: i32) {
|
||||
// The vehicle the player rode is gone (Entity.removeVehicle).
|
||||
if self.riding == Some(id as u32) {
|
||||
self.riding = None;
|
||||
}
|
||||
self.mobs.retain(|m| m.id != id as u32);
|
||||
self.orbs.retain(|o| o.id != id as u32);
|
||||
self.dropped.retain(|d| d.id != id as u32);
|
||||
@@ -1753,6 +1757,8 @@ impl Game<'_> {
|
||||
];
|
||||
self.move_entity(p.id, position, Some((p.yaw, p.pitch)), p.on_ground);
|
||||
}
|
||||
C::SetPassengers(p) => self.remote_set_passengers(p.vehicle, &p.passengers),
|
||||
C::MoveVehicle(p) => self.remote_move_vehicle(p.position, p.yaw),
|
||||
// ClientPacketListener.handleRotateMob: lerpHeadTo(yHeadRot, 3).
|
||||
C::RotateHead(p) => {
|
||||
if let Some(r) = self.remote.as_mut() {
|
||||
@@ -2932,9 +2938,9 @@ impl Game<'_> {
|
||||
self.capture_loaded_world();
|
||||
}
|
||||
let stick = self.pad_move();
|
||||
let Some(player) = self.player.as_mut() else {
|
||||
if self.player.is_none() {
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
let playing = self.screen == Screen::Playing;
|
||||
let held = |k| playing && self.keys.contains(&k);
|
||||
let keys = Keys {
|
||||
@@ -2955,7 +2961,14 @@ impl Game<'_> {
|
||||
| u8::from(keys.jump) << 4
|
||||
| u8::from(keys.shift) << 5
|
||||
| u8::from(keys.sprint) << 6;
|
||||
self.previous_position = player.position;
|
||||
// A passenger moves with its vehicle (Entity.rideTick), not by itself.
|
||||
let riding = self.screen != Screen::Dead && self.remote_ride(keys);
|
||||
let Some(player) = self.player.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
if !riding {
|
||||
self.previous_position = player.position;
|
||||
}
|
||||
let before = (
|
||||
player.ground.on_ground,
|
||||
[player.velocity.x, player.velocity.y, player.velocity.z],
|
||||
@@ -2972,7 +2985,7 @@ impl Game<'_> {
|
||||
f64::from(head[2]) + 0.5,
|
||||
);
|
||||
player.velocity = Vec3::ZERO;
|
||||
} else if !dead {
|
||||
} else if !dead && !riding {
|
||||
player.velocity = player.velocity.add_vector(pushed);
|
||||
MotionWorld {
|
||||
entities: &ghast_collisions,
|
||||
@@ -3258,6 +3271,9 @@ impl Game<'_> {
|
||||
};
|
||||
self.tick_remote_objects(&mut step);
|
||||
self.tick_remote_projectiles(&mut step);
|
||||
// LightningBolt.tick on a client, and the sky flash fading.
|
||||
self.flash = self.flash.saturating_sub(1);
|
||||
self.tick_bolts();
|
||||
}
|
||||
lerps.retain(|_, l| l.steps > 0);
|
||||
if let Some(r) = self.remote.as_mut() {
|
||||
@@ -3319,6 +3335,8 @@ impl Game<'_> {
|
||||
p.sprinting,
|
||||
);
|
||||
let (yaw, pitch, selected) = (self.yaw, self.pitch, self.selected);
|
||||
let riding = self.riding.is_some();
|
||||
let vehicle = self.remote_vehicle_packets();
|
||||
let Some(r) = self.remote.as_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -3341,9 +3359,18 @@ impl Game<'_> {
|
||||
data: 0,
|
||||
}));
|
||||
}
|
||||
if let Some(packet) = r
|
||||
.position_sender
|
||||
.update(pos, (yaw, pitch), on_ground, collision)
|
||||
if riding {
|
||||
// LocalPlayer.tick: a passenger sends its look, then the vehicle
|
||||
// it controls.
|
||||
out.push(S::MovePlayerRot(play::MovePlayerRot {
|
||||
yaw,
|
||||
pitch,
|
||||
flags: u8::from(on_ground) | u8::from(collision) << 1,
|
||||
}));
|
||||
out.extend(vehicle);
|
||||
} else if let Some(packet) =
|
||||
r.position_sender
|
||||
.update(pos, (yaw, pitch), on_ground, collision)
|
||||
{
|
||||
out.push(packet);
|
||||
}
|
||||
@@ -3574,6 +3601,21 @@ impl Game<'_> {
|
||||
|
||||
fn remote_use_entity(&mut self) -> bool {
|
||||
let sneaking = self.keys.contains(&KeyCode::ShiftLeft);
|
||||
// A boat, minecart, item frame, armor stand or painting nearer than
|
||||
// any mob: the server uses it (AbstractBoat.interact boards it).
|
||||
if let Some(hit) = self.remote_object_hit()
|
||||
&& self.mob_hit().is_none_or(|(_, d)| hit.distance < d)
|
||||
&& self.before_block(hit.distance)
|
||||
{
|
||||
self.send(S::Interact(play::Interact {
|
||||
id: hit.id,
|
||||
hand: 0,
|
||||
location: hit.location,
|
||||
secondary: sneaking,
|
||||
}));
|
||||
self.swing_arm();
|
||||
return true;
|
||||
}
|
||||
if let Some((i, distance)) = self.mob_hit()
|
||||
&& self.before_block(distance)
|
||||
{
|
||||
@@ -3953,6 +3995,14 @@ impl Game<'_> {
|
||||
id: self.creatures.projectiles[i].id as i32,
|
||||
}));
|
||||
self.reset_attack_strength();
|
||||
} else if let Some(hit) = self.remote_object_hit()
|
||||
&& self.mob_hit().is_none_or(|(_, d)| hit.distance < d)
|
||||
&& self.before_block(hit.distance)
|
||||
{
|
||||
// VehicleEntity.hurtServer, ItemFrame and ArmorStand hurtServer,
|
||||
// a painting broken: the server decides.
|
||||
self.send(S::Attack(play::Attack { id: hit.id }));
|
||||
self.reset_attack_strength();
|
||||
} else if let Some((i, distance)) = self.mob_hit()
|
||||
&& self.before_block(distance)
|
||||
{
|
||||
|
||||
@@ -52,6 +52,8 @@ impl Game<'_> {
|
||||
invisible: false,
|
||||
check_interval: 0,
|
||||
});
|
||||
} else if kind == "minecraft:lightning_bolt" {
|
||||
self.spawn_remote_bolt(id, position);
|
||||
} else if kind == "minecraft:painting" {
|
||||
// The variant comes with the entity data; until then the
|
||||
// registry's first one (VariantUtils.getAny).
|
||||
@@ -188,7 +190,12 @@ impl Game<'_> {
|
||||
/// A move's rotation and ground state for an object.
|
||||
pub(super) fn turn_remote_object(&mut self, id: i32, yaw: Option<f32>, on_ground: bool) {
|
||||
let id = id as u32;
|
||||
// The boat the player drives turns here (isLocalInstanceAuthoritative).
|
||||
let driven = self.riding;
|
||||
if let Some(b) = self.boats.iter_mut().find(|b| b.id == id) {
|
||||
if Some(id) == driven {
|
||||
return;
|
||||
}
|
||||
if let Some(yaw) = yaw {
|
||||
b.yaw = yaw;
|
||||
}
|
||||
@@ -213,7 +220,12 @@ impl Game<'_> {
|
||||
c.previous_yaw = c.yaw;
|
||||
c.position = step(c.id, c.position);
|
||||
}
|
||||
// The boat the player drives moves here, not where the server says.
|
||||
let driven = self.riding;
|
||||
for b in &mut self.boats {
|
||||
if Some(b.id) == driven {
|
||||
continue;
|
||||
}
|
||||
b.previous = b.position;
|
||||
b.previous_yaw = b.yaw;
|
||||
b.position = step(b.id, b.position);
|
||||
@@ -274,5 +286,6 @@ impl Game<'_> {
|
||||
self.armor_stands.retain(|a| a.id != id);
|
||||
self.item_frames.retain(|f| f.id != id);
|
||||
self.paintings.retain(|p| p.id != id);
|
||||
self.bolts.retain(|b| b.id != id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
//! Boats, minecarts, item frames, armor stands and paintings for a server's
|
||||
//! client: single player on its server thread, a LAN guest, a player on a
|
||||
//! dedicated server.
|
||||
//!
|
||||
//! The client aims at them like at mobs (`Minecraft.hitResult`, then
|
||||
//! `ServerboundInteractPacket`), and rides the vehicle the server seats it in
|
||||
//! (`ClientPacketListener.handleSetEntityPassengersPacket`). A boat is
|
||||
//! controlled by its rider's client: it ticks here and its moves go to the
|
||||
//! server (`LocalPlayer.tick`: `ServerboundMoveVehiclePacket`,
|
||||
//! `AbstractBoat.tick`: `ServerboundPaddleBoatPacket`). A minecart moves on
|
||||
//! the server with the rider's input, and the rider sits in it.
|
||||
use super::{Game, Keys, view_vector};
|
||||
use horizon_core::geometry::Vec3;
|
||||
use horizon_protocol::packets::play::{self, Serverbound as S};
|
||||
|
||||
/// An object under the crosshair: its entity id, how far along the view it
|
||||
/// was hit, and where (`EntityHitResult.getLocation` minus the entity's
|
||||
/// position, the interact packet's location).
|
||||
pub(super) struct ObjectHit {
|
||||
pub id: i32,
|
||||
pub distance: f64,
|
||||
pub location: [f64; 3],
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// The nearest boat, minecart, item frame, armor stand or painting the
|
||||
/// view hits within reach.
|
||||
pub(super) fn remote_object_hit(&self) -> Option<ObjectHit> {
|
||||
let eye = self.eye_position()?;
|
||||
let dir = view_vector(self.yaw, self.pitch);
|
||||
let at = |d: f64| {
|
||||
[
|
||||
eye[0] + dir[0] * d,
|
||||
eye[1] + dir[1] * d,
|
||||
eye[2] + dir[2] * d,
|
||||
]
|
||||
};
|
||||
let block_centre = |p: [i32; 3]| {
|
||||
[
|
||||
f64::from(p[0]) + 0.5,
|
||||
f64::from(p[1]) + 0.5,
|
||||
f64::from(p[2]) + 0.5,
|
||||
]
|
||||
};
|
||||
let mut hits: Vec<(i32, f64, [f64; 3])> = Vec::new();
|
||||
if let Some((i, d)) = self.painting_hit() {
|
||||
let p = &self.paintings[i];
|
||||
hits.push((p.id as i32, d, block_centre(p.pos)));
|
||||
}
|
||||
if let Some((i, d)) = self.frame_hit() {
|
||||
let f = &self.item_frames[i];
|
||||
hits.push((f.id as i32, d, block_centre(f.pos)));
|
||||
}
|
||||
if let Some((i, d, _)) = self.armor_stand_hit() {
|
||||
let a = &self.armor_stands[i];
|
||||
let p = a.position;
|
||||
hits.push((a.id as i32, d, [p.x, p.y, p.z]));
|
||||
}
|
||||
if let Some((i, d)) = self.boat_hit() {
|
||||
let p = self.boats[i].position;
|
||||
hits.push((self.boats[i].id as i32, d, [p.x, p.y, p.z]));
|
||||
}
|
||||
if let Some((i, d)) = self.cart_hit() {
|
||||
let p = self.carts[i].position;
|
||||
hits.push((self.carts[i].id as i32, d, [p.x, p.y, p.z]));
|
||||
}
|
||||
let (id, distance, origin) = hits.into_iter().min_by(|a, b| a.1.total_cmp(&b.1))?;
|
||||
let point = at(distance);
|
||||
Some(ObjectHit {
|
||||
id,
|
||||
distance,
|
||||
location: [
|
||||
point[0] - origin[0],
|
||||
point[1] - origin[1],
|
||||
point[2] - origin[2],
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// `ClientPacketListener.handleSetEntityPassengersPacket`: who rides
|
||||
/// `vehicle` now. The local player gets in or out; mobs and other
|
||||
/// players sit where the server keeps them.
|
||||
pub(super) fn remote_set_passengers(&mut self, vehicle: i32, passengers: &[i32]) {
|
||||
let Some(me) = self.remote.as_ref().map(|r| r.entity_id) else {
|
||||
return;
|
||||
};
|
||||
let vehicle_id = vehicle as u32;
|
||||
for m in &mut self.mobs {
|
||||
if m.extra.mount == Some(vehicle_id) && !passengers.contains(&(m.id as i32)) {
|
||||
m.extra.mount = None;
|
||||
} else if passengers.contains(&(m.id as i32)) {
|
||||
m.extra.mount = Some(vehicle_id);
|
||||
}
|
||||
}
|
||||
if passengers.contains(&me) {
|
||||
if self.riding != Some(vehicle_id) {
|
||||
self.riding = Some(vehicle_id);
|
||||
if let Some(p) = self.player.as_mut() {
|
||||
p.velocity = Vec3::ZERO;
|
||||
p.fall_distance = 0.0;
|
||||
}
|
||||
}
|
||||
} else if self.riding == Some(vehicle_id) {
|
||||
// The server takes the player out, then teleports it.
|
||||
self.riding = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The local player's tick in its vehicle, in place of its own movement:
|
||||
/// false when it rides nothing.
|
||||
pub(super) fn remote_ride(&mut self, keys: Keys) -> bool {
|
||||
let Some(id) = self.riding else {
|
||||
return false;
|
||||
};
|
||||
let seat = if let Some(i) = self.boats.iter().position(|b| b.id == id) {
|
||||
// AbstractBoat.tick on the controlling client: steer and move.
|
||||
let mut b = self.boats[i].clone();
|
||||
b.input = [keys.left, keys.right, keys.forward, keys.backward];
|
||||
let b = self.tick_boat(b, true);
|
||||
// positionRider: the seat turns with the boat; clampRotation.
|
||||
self.yaw += b.delta_rotation;
|
||||
let delta = (self.yaw - b.yaw + 180.0).rem_euclid(360.0) - 180.0;
|
||||
self.yaw += delta.clamp(-105.0, 105.0) - delta;
|
||||
let seat = b.position;
|
||||
self.boats[i] = b;
|
||||
Some(seat)
|
||||
} else {
|
||||
// A minecart: the server moves it, the rider sits in it.
|
||||
self.carts.iter().find(|c| c.id == id).map(|c| c.position)
|
||||
};
|
||||
if let Some(p) = self.player.as_mut() {
|
||||
self.previous_position = p.position;
|
||||
if let Some(v) = seat {
|
||||
p.position = Vec3::new(v.x, v.y + super::boats::SEAT, v.z);
|
||||
}
|
||||
p.velocity = Vec3::ZERO;
|
||||
p.fall_distance = 0.0;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// `LocalPlayer.tick` for a passenger: the boat it controls goes to the
|
||||
/// server every tick with its paddles (the rider's own position does not).
|
||||
pub(super) fn remote_vehicle_packets(&self) -> Vec<S> {
|
||||
let Some(b) = self
|
||||
.riding
|
||||
.and_then(|id| self.boats.iter().find(|b| b.id == id))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
vec![
|
||||
S::MoveVehicle(play::MoveVehicleRequest {
|
||||
position: [b.position.x, b.position.y, b.position.z],
|
||||
yaw: b.yaw,
|
||||
pitch: 0.0,
|
||||
on_ground: b.on_ground,
|
||||
}),
|
||||
S::PaddleBoat(play::PaddleBoat {
|
||||
left: b.paddling[0],
|
||||
right: b.paddling[1],
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
/// `ClientPacketListener.handleMoveVehicle`: the server put the boat the
|
||||
/// player drives back.
|
||||
pub(super) fn remote_move_vehicle(&mut self, position: [f64; 3], yaw: f32) {
|
||||
let Some(b) = self
|
||||
.riding
|
||||
.and_then(|id| self.boats.iter_mut().find(|b| b.id == id))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let at = Vec3::new(position[0], position[1], position[2]);
|
||||
b.previous = at;
|
||||
b.position = at;
|
||||
b.yaw = yaw;
|
||||
b.previous_yaw = yaw;
|
||||
b.velocity = Vec3::ZERO;
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,13 @@ pub struct Guest {
|
||||
/// The permission level (0 none, 2 game masters, 4 owners).
|
||||
pub permission: u8,
|
||||
pub sneaking: bool,
|
||||
/// The last `ServerboundPlayerInputPacket` flags (forward 1, backward 2,
|
||||
/// left 4, right 8, jump 16, shift 32, sprint 64).
|
||||
pub input: u8,
|
||||
/// The boat or minecart the player rides (`Entity.getVehicle`).
|
||||
pub vehicle: Option<u32>,
|
||||
/// `ServerGamePacketListenerImpl.lastGoodX/Y/Z` of the vehicle it controls.
|
||||
pub vehicle_good: Option<[f64; 3]>,
|
||||
pub sprinting: bool,
|
||||
pub fall_flying: bool,
|
||||
pub flying: bool,
|
||||
@@ -233,6 +240,9 @@ impl Guest {
|
||||
game_mode: SURVIVAL,
|
||||
permission: 0,
|
||||
sneaking: false,
|
||||
input: 0,
|
||||
vehicle: None,
|
||||
vehicle_good: None,
|
||||
sprinting: false,
|
||||
fall_flying: false,
|
||||
flying: false,
|
||||
|
||||
@@ -1968,6 +1968,7 @@ impl Hub {
|
||||
g.center = None;
|
||||
}
|
||||
S::PlayerInput(p) => {
|
||||
self.guests[i].input = p.flags;
|
||||
self.guests[i].sneaking = p.flags & 32 != 0;
|
||||
self.guests[i].sprinting = p.flags & 64 != 0 && p.flags & 1 != 0;
|
||||
}
|
||||
@@ -2050,6 +2051,11 @@ impl Hub {
|
||||
to[2].clamp(-3.0E7, 3.0E7),
|
||||
];
|
||||
let (yaw, pitch) = (wrap_degrees(yaw), wrap_degrees(pitch));
|
||||
// A passenger only turns: its vehicle carries it (absSnapTo where it is).
|
||||
if g.vehicle.is_some() {
|
||||
(g.yaw, g.pitch) = (yaw, pitch);
|
||||
return;
|
||||
}
|
||||
let d = [0, 1, 2].map(|k| to[k] - g.first_good[k]);
|
||||
let moved = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
|
||||
if g.sleeping {
|
||||
|
||||
@@ -672,6 +672,11 @@ packet!(
|
||||
/// ClientboundSetPassengersPacket: a vehicle and every entity riding it.
|
||||
SetPassengers { vehicle: i32 => VarInt, passengers: Vec<i32> => List<VarInt> }
|
||||
);
|
||||
packet!(
|
||||
/// ClientboundMoveVehiclePacket: the server puts a vehicle its rider
|
||||
/// controls back where it was (PositionAndRotation: x, y, z, yRot, xRot).
|
||||
MoveVehicle { position: [f64; 3] => Vec3d, yaw: f32 => Float, pitch: f32 => Float }
|
||||
);
|
||||
/// ClientboundSetEquipmentPacket: (EquipmentSlot ordinal, stack) pairs.
|
||||
pub struct Equipment;
|
||||
impl Codec<Vec<(u8, Option<Stack>)>> for Equipment {
|
||||
@@ -1529,6 +1534,7 @@ packet_enum!(Clientbound, crate::ids::play::clientbound {
|
||||
SetHeldSlot(SetHeldSlot) = SET_HELD_SLOT,
|
||||
SetObjective(SetObjective) = SET_OBJECTIVE,
|
||||
SetPassengers(SetPassengers) = SET_PASSENGERS,
|
||||
MoveVehicle(MoveVehicle) = MOVE_VEHICLE,
|
||||
ProjectilePower(ProjectilePower) = PROJECTILE_POWER,
|
||||
SetPlayerInventory(SetPlayerInventory) = SET_PLAYER_INVENTORY,
|
||||
SetPlayerTeam(SetPlayerTeam) = SET_PLAYER_TEAM,
|
||||
@@ -1802,6 +1808,15 @@ packet!(
|
||||
packet!(MovePlayerPosRot { x: f64 => Double, y: f64 => Double, z: f64 => Double, yaw: f32 => Float, pitch: f32 => Float, flags: u8 => UByte });
|
||||
packet!(MovePlayerRot { yaw: f32 => Float, pitch: f32 => Float, flags: u8 => UByte });
|
||||
packet!(MovePlayerStatus { flags: u8 => UByte });
|
||||
packet!(
|
||||
/// ServerboundMoveVehiclePacket: where the rider moved the vehicle it
|
||||
/// controls (PositionAndRotation, then onGround).
|
||||
MoveVehicleRequest { position: [f64; 3] => Vec3d, yaw: f32 => Float, pitch: f32 => Float, on_ground: bool => Bool }
|
||||
);
|
||||
packet!(
|
||||
/// ServerboundPaddleBoatPacket: the boat's left and right paddles.
|
||||
PaddleBoat { left: bool => Bool, right: bool => Bool }
|
||||
);
|
||||
packet!(PickItemFromBlock { pos: [i32; 3] => Pos, include_data: bool => Bool });
|
||||
packet!(
|
||||
/// ServerboundPlayerActionPacket.Action: 0 start destroy, 1 change destroy
|
||||
@@ -1869,6 +1884,8 @@ packet_enum!(Serverbound, crate::ids::play::serverbound {
|
||||
MovePlayerPosRot(MovePlayerPosRot) = MOVE_PLAYER_POS_ROT,
|
||||
MovePlayerRot(MovePlayerRot) = MOVE_PLAYER_ROT,
|
||||
MovePlayerStatus(MovePlayerStatus) = MOVE_PLAYER_STATUS_ONLY,
|
||||
MoveVehicle(MoveVehicleRequest) = MOVE_VEHICLE,
|
||||
PaddleBoat(PaddleBoat) = PADDLE_BOAT,
|
||||
PickItemFromBlock(PickItemFromBlock) = PICK_ITEM_FROM_BLOCK,
|
||||
PingRequest(crate::packets::status::Ping) = PING_REQUEST,
|
||||
PlayerAbilities(PlayerAbilitiesRequest) = PLAYER_ABILITIES,
|
||||
|
||||
+227
-53
@@ -116,8 +116,15 @@ address. The script then checks, from both sides, what a guest's game used to ge
|
||||
and wheat on farmland grows (random ticks), however the host runs its world.
|
||||
- ladder: the guest climbs down ten blocks of ladder and lands unhurt: the server resets its fall
|
||||
distance on a ladder every tick, as LivingEntity.handleOnClimbable does (#1255).
|
||||
- boats: the host (single player, on its server thread with --server-thread) right-clicks a
|
||||
boat on water and rides it, sneaks out, and breaks it with its fists (#1222).
|
||||
- boats: the host (single player, on its server thread or not) and the guest each right-click a
|
||||
boat on water and ride it, paddle it forward (the other side sees it move), sneak out,
|
||||
and break it with their fists (#1222).
|
||||
- minecarts: the host and the guest each get into a minecart on rails, sneak out beside it, and break
|
||||
it (#1222).
|
||||
- frames: the host and the guest each put a diamond in an item frame and turn it, and put a
|
||||
helmet on an armor stand and take it back (#1222).
|
||||
- lightning: a bolt summoned beside the host, then the guest: both sides get the bolt and flash the
|
||||
sky (on its server thread the host's window is a client of it too).
|
||||
- flight: a player in creative double-taps jump and flies, stays up, and stops flying in survival:
|
||||
the guest, and on the server thread the host's window too, take what may fly from the
|
||||
server's abilities (ClientPacketListener.handlePlayerAbilities).
|
||||
@@ -159,7 +166,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
GUEST_NAME = 'Guest'
|
||||
# The host plays single player on its server thread (--server-thread).
|
||||
SERVER_THREAD = False
|
||||
CHECKS = ['tab', 'self', 'armour', 'nametags', 'walls', 'chest', 'crafting', 'sign', 'damage', 'mobs', 'hazards', 'pvp', 'arrows', 'kills', 'hurt', 'respawn', 'containers', 'feedback', 'titles', 'chat', 'commands', 'recipes', 'machines', 'brewing', 'loom', 'stonecutter', 'grindstone', 'anvil', 'smithing', 'enchanting', 'beacon', 'crafter', 'lectern', 'cartography', 'trials', 'clicks', 'sleep', 'predict', 'push', 'eat', 'eating', 'xp', 'objects', 'projectiles', 'worn', 'blast', 'barter', 'deflect', 'fireball', 'light', 'time', 'swings', 'cracks', 'plates', 'effects', 'portals', 'distance', 'growth', 'flight', 'ladder', 'boats']
|
||||
CHECKS = ['tab', 'self', 'armour', 'nametags', 'walls', 'chest', 'crafting', 'sign', 'damage', 'mobs', 'hazards', 'pvp', 'arrows', 'kills', 'hurt', 'respawn', 'containers', 'feedback', 'titles', 'chat', 'commands', 'recipes', 'machines', 'brewing', 'loom', 'stonecutter', 'grindstone', 'anvil', 'smithing', 'enchanting', 'beacon', 'crafter', 'lectern', 'cartography', 'trials', 'clicks', 'sleep', 'predict', 'push', 'eat', 'eating', 'xp', 'objects', 'projectiles', 'worn', 'blast', 'barter', 'deflect', 'fireball', 'light', 'time', 'swings', 'cracks', 'plates', 'effects', 'portals', 'distance', 'growth', 'flight', 'ladder', 'boats', 'minecarts', 'frames', 'lightning']
|
||||
|
||||
|
||||
class Game:
|
||||
@@ -2312,50 +2319,222 @@ def check_ladder(host, guest, out):
|
||||
guest('command', command=f'tp @s {x + 0.5} {y} {z + 0.5}')
|
||||
|
||||
|
||||
def check_boats(host, guest, out):
|
||||
"""#1222: the single player boards a boat on water with a right click, sneaks out, and breaks it."""
|
||||
def apart(host, guest):
|
||||
"""The host where it stands and the guest 12 blocks east, each on a stone floor with room
|
||||
around: neither stands in the other's way (a ray from inside a player's box hits that player)."""
|
||||
x, y, z = (int(v // 1) for v in pos_of(host))
|
||||
bx = x + 3
|
||||
host('command', command='gamemode survival')
|
||||
host('command', command=f'fill {bx - 1} {y} {z - 1} {bx + 1} {y + 3} {z + 1} minecraft:air')
|
||||
host('command', command=f'fill {bx - 1} {y - 1} {z - 1} {bx + 1} {y - 1} {z + 1} minecraft:water')
|
||||
host('command', command=f'summon minecraft:oak_boat {bx + 0.5} {y - 0.1} {z + 0.5}')
|
||||
host.wait(30)
|
||||
boats = [o for o in host('status')['objects'] if o['kind'].endswith('oak_boat')
|
||||
and abs(o['position'][0] - (bx + 0.5)) < 2 and abs(o['position'][2] - (z + 0.5)) < 2]
|
||||
try:
|
||||
assert boats, 'the host sees no boat on the water'
|
||||
boat = boats[0]
|
||||
bp = boat['position']
|
||||
host.raw('look-at', f'{bp[0]:.2f}', f'{bp[1] + 0.3:.2f}', f'{bp[2]:.2f}')
|
||||
host.raw('mouse', 'right', 'click')
|
||||
host.wait(10)
|
||||
riding = host('status').get('riding')
|
||||
assert riding == boat['id'], f'right-clicking the boat {boat["id"]}: riding {riding}'
|
||||
host.raw('key', 'shift', 'down')
|
||||
host.wait(10)
|
||||
host.raw('key', 'shift', 'up')
|
||||
host.wait(10)
|
||||
riding = host('status').get('riding')
|
||||
assert riding is None, f'sneaking out of the boat: riding {riding}'
|
||||
host('command', command=f'tp @s {x + 0.5} {y} {z + 0.5}')
|
||||
host.wait(5)
|
||||
gone = False
|
||||
for _ in range(10):
|
||||
here = [o for o in host('status')['objects'] if o['id'] == boat['id']]
|
||||
if not here:
|
||||
gone = True
|
||||
spots = {}
|
||||
for label, player, dx in (('host', host, 0), ('guest', guest, 12)):
|
||||
px = x + dx
|
||||
host('command', command=f'fill {px - 3} {y - 1} {z - 3} {px + 4} {y - 1} {z + 6} minecraft:stone')
|
||||
host('command', command=f'fill {px - 3} {y} {z - 3} {px + 4} {y + 3} {z + 6} minecraft:air')
|
||||
player('command', command=f'tp @s {px + 0.5} {y} {z + 0.5} 0 0')
|
||||
spots[label] = (px, y, z)
|
||||
host.wait(10)
|
||||
guest.wait(10)
|
||||
return spots
|
||||
|
||||
|
||||
def board(player, kind, at):
|
||||
"""Right-click the `kind` object near `at` and check `player` rides it."""
|
||||
found = [o for o in player('status')['objects'] if o['kind'].endswith(kind)
|
||||
and abs(o['position'][0] - at[0]) < 2 and abs(o['position'][2] - at[2]) < 2]
|
||||
assert found, f'no {kind} near {at}'
|
||||
vehicle = found[0]
|
||||
vp = vehicle['position']
|
||||
player.raw('look-at', f'{vp[0]:.2f}', f'{vp[1] + 0.3:.2f}', f'{vp[2]:.2f}')
|
||||
player.raw('mouse', 'right', 'click')
|
||||
player.wait(10)
|
||||
riding = player('status').get('riding')
|
||||
assert riding == vehicle['id'], f'right-clicking the {kind} {vehicle["id"]}: riding {riding}'
|
||||
return vehicle
|
||||
|
||||
|
||||
def leave_vehicle(player, kind):
|
||||
player.raw('key', 'shift', 'down')
|
||||
player.wait(10)
|
||||
player.raw('key', 'shift', 'up')
|
||||
player.wait(10)
|
||||
riding = player('status').get('riding')
|
||||
assert riding is None, f'sneaking out of the {kind}: riding {riding}'
|
||||
|
||||
|
||||
def break_vehicle(player, vehicle_id, kind):
|
||||
"""Hit it with fists until it breaks: VehicleEntity.tick takes 1 off its damage every tick."""
|
||||
for _ in range(10):
|
||||
here = [o for o in player('status')['objects'] if o['id'] == vehicle_id]
|
||||
if not here:
|
||||
return
|
||||
vp = here[0]['position']
|
||||
player.raw('look-at', f'{vp[0]:.2f}', f'{vp[1] + 0.3:.2f}', f'{vp[2]:.2f}')
|
||||
player.raw('mouse', 'left', 'click')
|
||||
player.wait(2)
|
||||
raise AssertionError(f'ten hits left the {kind} in place')
|
||||
|
||||
|
||||
def check_boats(host, guest, out):
|
||||
"""#1222: the host (single player, on its server thread or not) and the guest each board a boat
|
||||
on water with a right click, paddle it forward (the other side sees it move), sneak out, and
|
||||
break it with their fists."""
|
||||
result = {}
|
||||
spots = apart(host, guest)
|
||||
for label, player, other in (('host', host, guest), ('guest', guest, host)):
|
||||
x, y, z = spots[label]
|
||||
bx = x + 3
|
||||
player('command', command='gamemode survival')
|
||||
# A pool ten blocks long in a stone basin (the world may float), with a path beside it to
|
||||
# walk back to the boat.
|
||||
player('command', command=f'fill {bx - 3} {y} {z - 2} {bx + 2} {y + 3} {z + 10} minecraft:air')
|
||||
player('command', command=f'fill {bx - 3} {y - 2} {z - 2} {bx + 2} {y - 1} {z + 10} minecraft:stone')
|
||||
player('command', command=f'fill {bx - 1} {y - 1} {z - 1} {bx + 1} {y - 1} {z + 9} minecraft:water')
|
||||
player('command', command=f'summon minecraft:oak_boat {bx + 0.5} {y - 0.1} {z + 0.5}')
|
||||
player.wait(30)
|
||||
try:
|
||||
boat = board(player, 'oak_boat', (bx + 0.5, y, z + 0.5))
|
||||
start = boat['position']
|
||||
# The summoned boat faces south: forward paddles it along the pool.
|
||||
player.raw('key', 'w', 'down')
|
||||
player.wait(30)
|
||||
player.raw('key', 'w', 'up')
|
||||
player.wait(20)
|
||||
mine = [o for o in player('status')['objects'] if o['id'] == boat['id']]
|
||||
assert mine, f'the {label} lost its boat'
|
||||
moved = math.dist(start, mine[0]['position'])
|
||||
assert moved > 0.5, f'paddling forward moved the {label}\'s boat {moved:.2f} blocks'
|
||||
# The other side sees the boat where its rider took it (the server accepted its moves).
|
||||
theirs = [o for o in objects_near(other, mine[0]['position'], 'objects')
|
||||
if o['kind'].endswith('oak_boat')]
|
||||
assert theirs, f'the other side sees no boat near the {label}\'s'
|
||||
far = min(math.dist(o['position'], mine[0]['position']) for o in theirs)
|
||||
assert far < 1.0, f'the other side sees the {label}\'s boat {far:.2f} blocks off'
|
||||
leave_vehicle(player, 'boat')
|
||||
player('command', command=f'tp @s {bx - 1.5} {y} {mine[0]["position"][2]:.2f}')
|
||||
player.wait(5)
|
||||
break_vehicle(player, boat['id'], 'boat')
|
||||
result[label] = {'boat': boat['id'], 'paddled': round(moved, 2), 'broken': True}
|
||||
finally:
|
||||
player('command', command=f'kill @e[type=minecraft:oak_boat,x={bx},y={y},z={z + 4},distance=..10]')
|
||||
player('command', command=f'fill {bx - 1} {y - 1} {z - 1} {bx + 1} {y - 1} {z + 9} minecraft:stone')
|
||||
player('command', command=f'tp @s {x + 0.5} {y} {z + 0.5}')
|
||||
player.wait(5)
|
||||
return result
|
||||
|
||||
|
||||
def check_minecarts(host, guest, out):
|
||||
"""#1222: the host and the guest each get into a minecart on rails with a right click, sneak out
|
||||
beside it, and break it with their fists."""
|
||||
result = {}
|
||||
spots = apart(host, guest)
|
||||
for label, player in (('host', host), ('guest', guest)):
|
||||
x, y, z = spots[label]
|
||||
cx = x + 3
|
||||
player('command', command='gamemode survival')
|
||||
player('command', command=f'fill {cx - 2} {y} {z - 2} {cx + 2} {y + 3} {z + 6} minecraft:air')
|
||||
player('command', command=f'fill {cx - 2} {y - 1} {z - 2} {cx + 2} {y - 1} {z + 6} minecraft:stone')
|
||||
player('command', command=f'fill {cx} {y} {z} {cx} {y} {z + 4} minecraft:rail')
|
||||
player('command', command=f'summon minecraft:minecart {cx + 0.5} {y} {z + 0.5}')
|
||||
player.wait(30)
|
||||
try:
|
||||
cart = board(player, 'minecart', (cx + 0.5, y, z + 0.5))
|
||||
seated = pos_of(player)
|
||||
assert abs(seated[0] - (cx + 0.5)) < 0.6, f'the {label} sits at {seated}, not in the cart'
|
||||
leave_vehicle(player, 'minecart')
|
||||
out_at = pos_of(player)
|
||||
assert math.dist(out_at, seated) > 0.3, f'the {label} stayed in its seat at {out_at}'
|
||||
player.wait(5)
|
||||
break_vehicle(player, cart['id'], 'minecart')
|
||||
result[label] = {'cart': cart['id'], 'out_at': [round(v, 2) for v in out_at], 'broken': True}
|
||||
finally:
|
||||
player('command', command=f'kill @e[type=minecraft:minecart,x={cx},y={y},z={z},distance=..8]')
|
||||
player('command', command=f'kill @e[type=minecraft:item,x={cx},y={y},z={z},distance=..8]')
|
||||
player('command', command=f'fill {cx} {y} {z} {cx} {y} {z + 4} minecraft:air')
|
||||
player('command', command=f'tp @s {x + 0.5} {y} {z + 0.5}')
|
||||
player.wait(5)
|
||||
return result
|
||||
|
||||
|
||||
def check_frames(host, guest, out):
|
||||
"""#1222: the host and the guest each put a diamond in an item frame and turn it, and put a
|
||||
helmet on an armor stand and take it back, with right clicks."""
|
||||
result = {}
|
||||
spots = apart(host, guest)
|
||||
for label, player, other in (('host', host, guest), ('guest', guest, host)):
|
||||
x, y, z = spots[label]
|
||||
player('command', command='gamemode survival')
|
||||
# Within a survival player's 3 blocks of reach: a frame on a wall, an armor stand beside it.
|
||||
player('command', command=f'fill {x - 2} {y} {z + 1} {x + 2} {y + 2} {z + 2} minecraft:air')
|
||||
player('command', command=f'fill {x - 2} {y} {z + 3} {x + 2} {y + 2} {z + 3} minecraft:stone')
|
||||
player('command', command=f'summon minecraft:item_frame {x + 0.5} {y + 1.5} {z + 2.97} '
|
||||
f'{{block_pos:[I;{x},{y + 1},{z + 2}],Facing:2b}}')
|
||||
player('command', command=f'summon minecraft:armor_stand {x - 1.5} {y} {z + 1.5}')
|
||||
player('command', command='clear @s')
|
||||
player('command', command='item replace entity @s weapon.mainhand with minecraft:diamond')
|
||||
player.wait(30)
|
||||
try:
|
||||
frame = [o for o in player('status')['objects'] if o['kind'].endswith('item_frame')
|
||||
and o['position'] == [x, y + 1, z + 2]]
|
||||
assert frame, f'the {label} sees no item frame at {[x, y + 1, z + 2]}'
|
||||
player.raw('look-at', f'{x + 0.5:.2f}', f'{y + 1.5:.2f}', f'{z + 2.95:.2f}')
|
||||
player.raw('mouse', 'right', 'click')
|
||||
player.wait(10)
|
||||
player.raw('mouse', 'right', 'click')
|
||||
player.wait(10)
|
||||
frame = [o for o in player('status')['objects'] if o['id'] == frame[0]['id']][0]
|
||||
assert frame['item'] == 'minecraft:diamond', f'the {label}\'s frame holds {frame["item"]}'
|
||||
assert frame['rotation'] == 1, f'the {label}\'s frame turned to {frame["rotation"]}'
|
||||
theirs = [o for o in other('status')['objects'] if o['kind'].endswith('item_frame')
|
||||
and o['position'] == [x, y + 1, z + 2]]
|
||||
assert theirs and theirs[0]['item'] == 'minecraft:diamond' and theirs[0]['rotation'] == 1, \
|
||||
f'the other side sees the {label}\'s frame as {theirs}'
|
||||
player('command', command='item replace entity @s weapon.mainhand with minecraft:iron_helmet')
|
||||
player.wait(5)
|
||||
stand = [o for o in player('status')['objects'] if o['kind'] == 'minecraft:armor_stand'
|
||||
and abs(o['position'][0] - (x - 1.5)) < 0.5 and abs(o['position'][2] - (z + 1.5)) < 0.5]
|
||||
assert stand, f'the {label} sees no armor stand'
|
||||
player.raw('look-at', f'{x - 1.5:.2f}', f'{y + 1.7:.2f}', f'{z + 1.5:.2f}')
|
||||
player.raw('mouse', 'right', 'click')
|
||||
player.wait(10)
|
||||
worn = [o for o in player('status')['objects'] if o['id'] == stand[0]['id']][0]['equipment']
|
||||
assert 'minecraft:iron_helmet' in worn, f'the {label}\'s armor stand wears {worn}'
|
||||
player.raw('look-at', f'{x - 1.5:.2f}', f'{y + 1.7:.2f}', f'{z + 1.5:.2f}')
|
||||
player.raw('mouse', 'right', 'click')
|
||||
player.wait(10)
|
||||
worn = [o for o in player('status')['objects'] if o['id'] == stand[0]['id']][0]['equipment']
|
||||
assert 'minecraft:iron_helmet' not in worn, f'taking the helmet back left {worn}'
|
||||
result[label] = {'frame': 'diamond, turned', 'stand': 'helmet on and off'}
|
||||
finally:
|
||||
player('command', command=f'kill @e[type=!minecraft:player,x={x - 3},y={y - 1},z={z},dx=6,dy=4,dz=6]')
|
||||
player('command', command=f'fill {x - 2} {y} {z + 3} {x + 2} {y + 2} {z + 3} minecraft:air')
|
||||
player('command', command='clear @s')
|
||||
return result
|
||||
|
||||
|
||||
def check_lightning(host, guest, out):
|
||||
"""A bolt summoned beside each player: both sides get the bolt entity and flash the sky (the
|
||||
host's window too on its server thread, FEATURES 4z(f))."""
|
||||
result = {}
|
||||
for label, player in (('host', host), ('guest', guest)):
|
||||
x, y, z = (int(v // 1) for v in pos_of(player))
|
||||
before = {g.name: g('status')['lightning'] for g in (host, guest)}
|
||||
# No fire where it strikes: on stone with nothing around.
|
||||
player('command', command=f'fill {x + 4} {y - 1} {z - 2} {x + 8} {y - 1} {z + 2} minecraft:stone')
|
||||
player('command', command=f'fill {x + 4} {y} {z - 2} {x + 8} {y + 3} {z + 2} minecraft:air')
|
||||
player('command', command=f'summon minecraft:lightning_bolt {x + 6.5} {y} {z + 0.5}')
|
||||
seen = {}
|
||||
for _ in range(40):
|
||||
player.wait(1)
|
||||
seen = {g.name: g('status')['lightning'] for g in (host, guest)}
|
||||
if all(seen[n]['struck'] > before[n]['struck'] and seen[n]['flashes'] > before[n]['flashes']
|
||||
for n in seen):
|
||||
break
|
||||
bp = here[0]['position']
|
||||
host.raw('look-at', f'{bp[0]:.2f}', f'{bp[1] + 0.3:.2f}', f'{bp[2]:.2f}')
|
||||
host.raw('mouse', 'left', 'click')
|
||||
# VehicleEntity.tick takes 1 off the damage every tick: hit fast.
|
||||
host.wait(2)
|
||||
assert gone, 'ten hits left the boat in place'
|
||||
return {'boat': boat['id'], 'rode': True, 'broken': True}
|
||||
finally:
|
||||
host('command', command=f'kill @e[type=minecraft:oak_boat,x={bx},y={y},z={z},distance=..6]')
|
||||
host('command', command=f'fill {bx - 1} {y - 1} {z - 1} {bx + 1} {y - 1} {z + 1} minecraft:stone')
|
||||
player('command', command=f'fill {x + 4} {y} {z - 2} {x + 8} {y + 3} {z + 2} minecraft:air')
|
||||
for n in seen:
|
||||
assert seen[n]['struck'] > before[n]['struck'], f"{label}'s bolt: the {n} got no bolt ({seen[n]})"
|
||||
assert seen[n]['flashes'] > before[n]['flashes'], f"{label}'s bolt: the {n}'s sky didn't flash ({seen[n]})"
|
||||
result[label] = {n: {'bolts': seen[n]['struck'] - before[n]['struck'],
|
||||
'flashes': seen[n]['flashes'] - before[n]['flashes']} for n in seen}
|
||||
return result
|
||||
|
||||
|
||||
def check_growth(host, guest, out):
|
||||
"""Block ticks near a guest 1000 blocks from the host: only the guest's tickets tick them."""
|
||||
@@ -2669,18 +2848,13 @@ CHECK_FNS = {'tab': check_tab, 'self': check_self, 'armour': check_armour, 'name
|
||||
'xp': check_xp, 'objects': check_objects, 'projectiles': check_projectiles, 'blast': check_blast,
|
||||
'light': check_light, 'plates': check_plates,
|
||||
'swings': check_swings, 'cracks': check_cracks,
|
||||
'time': check_time, 'growth': check_growth, 'flight': check_flight, 'fireball': check_fireball, 'deflect': check_deflect, 'barter': check_barter, 'worn': check_worn, 'kills': check_kills, 'arrows': check_arrows, 'predict': check_predict, 'ladder': check_ladder, 'boats': check_boats}
|
||||
'time': check_time, 'growth': check_growth, 'flight': check_flight, 'fireball': check_fireball, 'deflect': check_deflect, 'barter': check_barter, 'worn': check_worn, 'kills': check_kills, 'arrows': check_arrows, 'predict': check_predict, 'ladder': check_ladder, 'boats': check_boats, 'minecarts': check_minecarts, 'frames': check_frames, 'lightning': check_lightning}
|
||||
|
||||
|
||||
def run(ctl, out, resources, checks):
|
||||
host, guest = Game('host', ctl, resources), Game('guest', ctl, resources)
|
||||
# Without --server-thread the host runs the game's default, the world in the frame, as players
|
||||
# get it (`boats` fails there if the default goes back to the server thread before it has
|
||||
# vehicles, #1222).
|
||||
if SERVER_THREAD:
|
||||
host.d.env['HORIZON_SERVER_THREAD'] = '1'
|
||||
else:
|
||||
host.d.env.pop('HORIZON_SERVER_THREAD', None)
|
||||
# Either way explicitly: the game's default is the server thread.
|
||||
host.d.env['HORIZON_SERVER_THREAD'] = '1' if SERVER_THREAD else '0'
|
||||
results = {}
|
||||
try:
|
||||
host.pid = host('launch', wait=False, offscreen=sys.platform == 'darwin')['pid']
|
||||
|
||||
+15
-3
@@ -15,6 +15,9 @@ is off: nothing leaves the machine.
|
||||
|
||||
python3 tools/test_friends.py --client target/fast/horizon-client --relay target/fast/horizon-relay
|
||||
|
||||
`--server-thread` runs the host's world on its integrated server's own
|
||||
thread (the window is a client of it, as Open to LAN publishes it).
|
||||
|
||||
`--role host|guest` runs one side only (two machines, e.g. over a tailnet):
|
||||
the host prints the code, the guest takes it with `--code` (`--relay-url`
|
||||
for a relay elsewhere, `--path relay` to force it). Needs a display on Linux
|
||||
@@ -86,8 +89,12 @@ class Game:
|
||||
# Extra words before and after the client (`--xvfb`, `--offscreen`).
|
||||
prefix = []
|
||||
extra = []
|
||||
# The host's world on its server thread (`--server-thread`), else in the window.
|
||||
server_thread = False
|
||||
|
||||
def __init__(self, client, resources, work, name, env):
|
||||
# Either way explicitly, whatever the game's default.
|
||||
env = dict(env, HORIZON_SERVER_THREAD='1' if Game.server_thread and name == 'host' else '0')
|
||||
self.home = work / name
|
||||
self.home.mkdir()
|
||||
self.endpoint = self.home / 'c.sock'
|
||||
@@ -155,7 +162,9 @@ def play_together(host, guest, name, round_index):
|
||||
# The host moves; the guest sees the host's player there.
|
||||
hp = host('status')['player']['position']
|
||||
target = [hp[0] + 3.0, hp[1], hp[2] + 1.0]
|
||||
host(f'tp {target[0]} {target[1]} {target[2]}')
|
||||
# The /tp command, not the control socket's local move: on the server thread the window is a
|
||||
# client, and its server has the last word on where the host stands.
|
||||
host(f'command tp @s {target[0]} {target[1]} {target[2]}')
|
||||
guest.wait('status', lambda v: any(math.dist(p, target) < 0.6 for p in v['server']['player_positions']), 20,
|
||||
'host movement on the guest')
|
||||
# A block the host places, which the guest sees and then breaks.
|
||||
@@ -163,8 +172,9 @@ def play_together(host, guest, name, round_index):
|
||||
feet = [math.floor(gp[0]), math.floor(gp[1]), math.floor(gp[2])]
|
||||
block = [feet[0] + 2, feet[1] + 1, feet[2]]
|
||||
# Clear the line of sight (the test terrain has hills), then the block at eye level.
|
||||
host(f'fill {feet[0] + 1} {feet[1]} {feet[2]} {feet[0] + 1} {feet[1] + 2} {feet[2]} minecraft:air')
|
||||
host(f'setblock {block[0]} {block[1]} {block[2]} minecraft:gold_block')
|
||||
# As commands, for the same reason as the /tp above.
|
||||
host(f'command fill {feet[0] + 1} {feet[1]} {feet[2]} {feet[0] + 1} {feet[1] + 2} {feet[2]} minecraft:air')
|
||||
host(f'command setblock {block[0]} {block[1]} {block[2]} minecraft:gold_block')
|
||||
guest.wait(f'getblock {block[0]} {block[1]} {block[2]}', lambda v: v['block'] == 'minecraft:gold_block', 20,
|
||||
'the host block on the guest')
|
||||
eye = [gp[0], gp[1] + 1.62, gp[2]]
|
||||
@@ -343,10 +353,12 @@ def main():
|
||||
parser.add_argument('--hold', type=float, default=0, help='guest: stay this many seconds before leaving')
|
||||
parser.add_argument('--baseline', action='store_true', help='first run the same checks over plain LAN')
|
||||
parser.add_argument('--offscreen', action='store_true', help='run the games without a window')
|
||||
parser.add_argument('--server-thread', action='store_true', help="the host's world on its server thread")
|
||||
parser.add_argument('--xvfb', action='store_true', help='run the games under xvfb-run (headless Linux)')
|
||||
args = parser.parse_args()
|
||||
Game.prefix = ['xvfb-run', '-a'] if args.xvfb else []
|
||||
Game.extra = ['--offscreen'] if args.offscreen else []
|
||||
Game.server_thread = args.server_thread
|
||||
args.client, args.resources = args.client.resolve(), args.resources.resolve()
|
||||
# Short: Unix socket paths are limited to about 100 bytes.
|
||||
with tempfile.TemporaryDirectory(prefix='hzf-', dir='/tmp') as directory:
|
||||
|
||||
Reference in new issue
Block a user