Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / client / src / entities / mod.rs

use crate::world::ClientWorld;
use common::JavaRandom;
use common::entity::mob::MobKind;
use common::entity::player::{self, PlayerState};
use common::entity::vehicle::VehicleKind;
use common::entity::{Entity, flags};
use common::inventory::PlayerInventory;
use common::item::{Item, ItemStack};
use common::math::floor_f64;
use common::physics::Aabb;
use common::protocol::{Metadata, MetadataValue, Packet};
use common::world::shape::BlockView;
use common::world::{Block, Material};
use glam::DVec3;
use rustc_hash::FxHashMap;

#[derive(Clone, Copy)]
pub enum Kind {
    Player,
    Painting(common::entity::painting::Painting),
    Item(ItemStack),
    Falling(Block),
    Mob(MobKind),
    Arrow,
    Fireball,
    Hook(Option<i32>),
    Vehicle(VehicleKind),
    /// A snowball or egg in flight, drawn as its item icon.
    Thrown(Item),
    Tnt,
}

#[derive(Clone, Copy, Default)]
pub struct Animation {
    pub rotation: [f32; 3],
    pub walk: [f32; 2],
    pub hurt: i32,
    pub death: i32,
    pub fuse: i32,
    pub special: common::entity::mob::SpecialPose,
    pub vehicle: common::entity::vehicle::VehiclePose,
}

pub struct RemotePlayer {
    pub name: String,
    pub entity: Entity,
    pub inventory: PlayerInventory,
    state: PlayerState,
}

pub struct RemoteEntity {
    pub player: Option<Box<RemotePlayer>>,
    pub riding: Option<i32>,
    pub kind: Kind,
    pub position: DVec3,
    pub previous: DVec3,
    pub age: u32,
    pub flags: [u8; 3],
    pub animation: Animation,
    pub previous_animation: Animation,
    pending: Option<(DVec3, Animation)>,
    target: DVec3,
    target_rotation: [f32; 2],
    remaining: u32,
    /// `Entity.outfitWithItem`: the held item, then the armor from boots to helmet.
    pub equipment: [Option<ItemStack>; 5],
    /// `serverPosX` and its siblings: the last position the server sent, in 1/32 blocks.
    server_position: [i32; 3],
    /// Animated on the client, as a Java server sends no `EntityState`.
    remote: bool,
    /// `EntityArrow.arrowShake`: the wobble of an arrow that just stuck.
    pub shake: i32,
    moving: bool,
    velocity: DVec3,
    acceleration: DVec3,
    squid: Option<(common::entity::mob::SquidMotion, JavaRandom)>,
    grounded: bool,
    /// `Entity.rand`, for effects the client makes itself.
    rand: JavaRandom,
    /// `EntityLiving.livingSoundTime`.
    living_sound: i32,
    /// Flying into the collector after a `Collect`: its id, the ticks so far and the start.
    collected: Option<(i32, u32, DVec3)>,
    /// `EntityWolf.isShaking`: shaking itself dry.
    shaking: bool,
}

/// A sound: name, position, volume and a pitch, or a random one.
pub type Sound = (&'static str, DVec3, f32, Option<f32>);
/// A particle: name, position and velocity.
pub type Effect = (&'static str, DVec3, DVec3);

#[derive(Default)]
pub struct Entities(
    pub FxHashMap<i32, RemoteEntity>,
    pub Vec<Sound>,
    pub Vec<Effect>,
);

fn angle(byte: i8) -> f32 {
    f32::from(byte) * 360.0 / 256.0
}

pub fn velocity(units: [i16; 3]) -> DVec3 {
    DVec3::from_array(units.map(|v| f64::from(v) / 8000.0))
}

/// The small random velocity of a puff of particles.
fn drift(random: &mut JavaRandom) -> DVec3 {
    DVec3::new(
        random.next_gaussian(),
        random.next_gaussian(),
        random.next_gaussian(),
    ) * 0.02
}

fn in_water(world: &ClientWorld, position: DVec3) -> bool {
    let [x, y, z] = position.to_array().map(floor_f64);
    world.block(x, y, z).material() == Material::Water
}

impl RemoteEntity {
    /// A freshly spawned entity at `position`, in 1/32 blocks.
    pub fn new(id: i32, kind: Kind, position: [i32; 3]) -> Self {
        let position_units = position;
        let position = DVec3::from_array(position.map(f64::from)) / 32.0;
        Self {
            player: None,
            riding: None,
            kind,
            position,
            previous: position,
            age: 0,
            flags: [0; 3],
            animation: Animation::default(),
            previous_animation: Animation::default(),
            pending: None,
            target: position,
            target_rotation: [0.0; 2],
            remaining: 0,
            shake: 0,
            moving: false,
            velocity: DVec3::ZERO,
            acceleration: DVec3::ZERO,
            squid: matches!(kind, Kind::Mob(MobKind::Squid)).then(|| {
                let mut random = JavaRandom::new(i64::from(id));
                let mut state = common::entity::mob::SquidMotion::default();
                state.initialize(&mut random);
                (state, random)
            }),
            grounded: false,
            server_position: position_units,
            remote: false,
            equipment: [None; 5],
            rand: JavaRandom::new(i64::from(id) ^ 0x5DEE_CE66),
            living_sound: 0,
            collected: None,
            shaking: false,
        }
    }

    fn metadata(&mut self, metadata: &Metadata) {
        for (id, value) in metadata {
            if *id == 18
                && let MetadataValue::Int(health) = value
            {
                self.flags[2] = (*health).clamp(0, 255) as u8;
            }
            if let MetadataValue::Byte(value) = value {
                let index = match id {
                    0 => 0,
                    16 => 1,
                    17 => 2,
                    _ => continue,
                };
                self.flags[index] = *value as u8;
            }
        }
    }

    pub fn bounds(&self) -> Aabb {
        if let Kind::Painting(painting) = self.kind {
            return painting.bounds();
        }
        let [width, height] = match self.kind {
            Kind::Mob(MobKind::Slime) => [0.6 * f32::from(self.flags[1].max(1)); 2],
            Kind::Mob(kind) => kind.size(),
            Kind::Player => [0.6, 1.8],
            Kind::Fireball => [1.0; 2],
            Kind::Vehicle(kind) => kind.size(),
            Kind::Falling(_) | Kind::Tnt => [0.98, 0.98],
            _ => [0.25, 0.25],
        };
        let half = f64::from(width) / 2.0;
        let offset = match self.kind {
            Kind::Vehicle(kind) => f64::from(kind.y_offset()),
            Kind::Falling(_) | Kind::Tnt => f64::from(0.98_f32 / 2.0),
            _ => 0.0,
        };
        Aabb::new(
            self.position - DVec3::new(half, offset, half),
            self.position + DVec3::new(half, f64::from(height) - offset, half),
        )
    }

    /// Faces `yaw` and `pitch`, in 1/256 turns, as a mob or player the server moves.
    fn face(&mut self, yaw: i8, pitch: i8) {
        let [yaw, pitch] = [angle(yaw), angle(pitch)];
        self.animation.rotation = [yaw, pitch, yaw];
        self.previous_animation = self.animation;
        self.target_rotation = [yaw, pitch];
        self.remote = true;
    }

    fn interpolation_steps(&self) -> u32 {
        match self.kind {
            Kind::Vehicle(VehicleKind::Boat) => 7,
            Kind::Vehicle(_) => 5,
            // `Entity.setPositionAndRotation2` puts other things right there
            Kind::Item(_) | Kind::Falling(_) | Kind::Tnt | Kind::Arrow | Kind::Thrown(_) => 1,
            _ => 3,
        }
    }

    /// `EntityWolf.onUpdate`: soaked by water or rain, then shaking itself dry.
    fn wolf_update(
        &mut self,
        world: &ClientWorld,
        sounds: &mut Vec<Sound>,
        effects: &mut Vec<Effect>,
    ) {
        let [x, y, z] = self.position.to_array().map(floor_f64);
        let special = &mut self.animation.special;
        if in_water(world, self.position)
            || in_water(world, self.position + DVec3::Y * 0.5)
            || world.rained_on(x, y, z)
        {
            special.wet = true;
            self.shaking = false;
            special.shake = 0.0;
        } else if self.shaking {
            if special.shake == 0.0 {
                sounds.push(("mob.wolf.shake", self.position, 0.4, None));
            }
            let previous = special.shake;
            special.shake += 0.05;
            if previous >= 2.0 {
                special.wet = false;
                self.shaking = false;
                special.shake = 0.0;
            }
            if special.shake > 0.4 {
                let count = ((special.shake - 0.4) * std::f32::consts::PI).sin() * 7.0;
                let width = MobKind::Wolf.size()[0];
                for _ in 0..count as i32 {
                    let mut offset =
                        || f64::from((self.rand.next_float() * 2.0 - 1.0) * width * 0.5);
                    let (dx, dz) = (offset(), offset());
                    let y = self.bounds().min.y + 0.8;
                    let pos = DVec3::new(self.position.x + dx, y, self.position.z + dz);
                    effects.push(("splash", pos, self.position - self.previous));
                }
            }
        }
    }

    fn death_puff(&mut self, kind: MobKind, effects: &mut Vec<Effect>) {
        let [width, height] = kind.size();
        for _ in 0..20 {
            let random = &mut self.rand;
            let velocity = drift(random);
            let pos = self.position
                + DVec3::new(
                    f64::from(random.next_float() * width * 2.0 - width),
                    f64::from(random.next_float() * height),
                    f64::from(random.next_float() * width * 2.0 - width),
                );
            effects.push(("explode", pos, velocity));
        }
    }

    /// `getLivingSound`, which for wolves depends on their mood and health.
    fn living_sound_name(&mut self, kind: MobKind) -> Option<&'static str> {
        if kind != MobKind::Wolf {
            return kind.sound(false, false);
        }
        let [_, mood, health] = self.flags;
        Some(if mood & 2 != 0 {
            "mob.wolf.growl"
        } else if self.rand.next_int(3) == 0 {
            if mood & 4 != 0 && health < 10 {
                "mob.wolf.whine"
            } else {
                "mob.wolf.panting"
            }
        } else {
            "mob.wolf.bark"
        })
    }

    /// The client's own `onUpdate` of items, falling blocks, TNT, arrows, snowballs and eggs
    /// between the server's position updates.
    fn simulate(&mut self, world: &mut ClientWorld, effects: &mut Vec<Effect>) {
        let gravity_body = match self.kind {
            Kind::Item(_) => Some((0.25, 0.125)),
            Kind::Falling(_) | Kind::Tnt => Some((0.98, 0.49)),
            _ => None,
        };
        if let Some((size, offset)) = gravity_body {
            self.velocity.y -= f64::from(0.04_f32);
            let mut probe = Entity::new(0, size, size, JavaRandom::new(0));
            probe.y_offset = offset;
            probe.triggers_walking = false;
            probe.set_position(self.position);
            probe.move_entity(world, self.velocity);
            self.position = probe.pos;
            let friction = match self.kind {
                Kind::Item(_) if probe.on_ground => {
                    let [x, y, z] = [probe.pos.x, probe.bb.min.y - 1.0, probe.pos.z].map(floor_f64);
                    world.block(x, y, z).slipperiness() * 0.98
                }
                _ => 0.98,
            };
            let [friction, drag] = [friction, 0.98_f32].map(f64::from);
            self.velocity *= DVec3::new(friction, drag, friction);
            if probe.on_ground {
                if !matches!(self.kind, Kind::Item(_)) {
                    self.velocity.x *= f64::from(0.7_f32);
                    self.velocity.z *= f64::from(0.7_f32);
                }
                self.velocity.y *= -0.5;
            }
            if matches!(self.kind, Kind::Tnt) {
                let fuse = self.animation.fuse;
                self.animation.fuse -= 1;
                if fuse > 0 {
                    effects.push(("smoke", self.position + DVec3::Y * 0.5, DVec3::ZERO));
                }
            }
            return;
        }
        if self.grounded || !matches!(self.kind, Kind::Arrow | Kind::Thrown(_)) {
            return;
        }
        let end = self.position + self.velocity;
        match common::world::shape::ray_trace(&*world, self.position, end, false, true) {
            Some(hit) if matches!(self.kind, Kind::Arrow) => {
                self.position = hit.point;
                self.velocity = DVec3::ZERO;
                self.grounded = true;
                return;
            }
            _ => self.position = end,
        }
        let drag = if in_water(world, self.position) {
            0.8_f32
        } else {
            0.99_f32
        };
        self.velocity *= f64::from(drag);
        self.velocity.y -= f64::from(0.03_f32);
        if matches!(self.kind, Kind::Arrow) {
            let horizontal =
                (self.velocity.x * self.velocity.x + self.velocity.z * self.velocity.z).sqrt();
            self.animation.rotation[0] = self.velocity.x.atan2(self.velocity.z).to_degrees() as f32;
            self.animation.rotation[1] = self.velocity.y.atan2(horizontal).to_degrees() as f32;
        }
    }

    /// The parts of `EntityLiving.onUpdate` the server leaves to the client: limb swing, the
    /// body turning towards the head, and the hurt and death timers.
    fn animate(&mut self) {
        let animation = &mut self.animation;
        let delta = self.position - self.previous;
        let distance = ((delta.x * delta.x + delta.z * delta.z).sqrt() * 4.0).min(1.0) as f32;
        animation.walk[1] += (distance - animation.walk[1]) * 0.4;
        animation.walk[0] += animation.walk[1];
        // `EntityLiving.onUpdate`: the body turns where it walks, never far from the head
        let wrap = |angle: f32| (angle + 180.0).rem_euclid(360.0) - 180.0;
        let [head, _, body] = &mut animation.rotation;
        let mut heading = *body;
        if (delta.x * delta.x + delta.z * delta.z).sqrt() > 0.05 {
            heading = delta.z.atan2(delta.x).to_degrees() as f32 - 90.0;
        }
        if self
            .player
            .as_ref()
            .and_then(|p| p.entity.living.as_ref())
            .is_some_and(|l| l.swing_progress > 0.0)
        {
            heading = *head;
        }
        *body += wrap(heading - *body) * 0.3;
        let offset = wrap(*head - *body).clamp(-75.0, 75.0);
        *body = *head - offset;
        if offset * offset > 2500.0 {
            *body += offset * 0.2;
        }
        animation.hurt = (animation.hurt - 1).max(0);
        if animation.death > 0 {
            animation.death += 1;
        }
    }
}

impl Entities {
    pub fn receive(&mut self, packet: &Packet, world: &ClientWorld) -> bool {
        match packet {
            Packet::Painting {
                entity,
                title,
                x,
                y,
                z,
                direction,
            } => {
                let painting = common::entity::painting::Painting::from_name(
                    title,
                    [*x, *y, *z],
                    *direction as u8,
                );
                let entity = self.spawn(*entity, Kind::Painting(painting), [0; 3]);
                entity.position = painting.position();
                entity.previous = entity.position;
                entity.target = entity.position;
                let turn = f32::from(painting.direction) * 90.0;
                entity.animation.rotation = [turn, 0.0, turn];
                entity.previous_animation = entity.animation;
            }
            Packet::NamedEntitySpawn {
                entity,
                name,
                x,
                y,
                z,
                yaw,
                pitch,
                item,
            } => {
                let remote = self.spawn(*entity, Kind::Player, [*x, *y, *z]);
                remote.face(*yaw, *pitch);
                let mut model = Entity::new_player(*entity, JavaRandom::new(i64::from(*entity)));
                model.y_offset = 0.0;
                let [yaw, pitch, _] = remote.animation.rotation;
                model.set_location_and_angles(remote.position, yaw, pitch);
                model.prev_pos = model.pos;
                model.prev_yaw = model.yaw;
                model.prev_pitch = model.pitch;
                if let Some(living) = &mut model.living {
                    living.render_yaw_offset = model.yaw;
                    living.prev_render_yaw_offset = model.yaw;
                }
                let mut inventory = PlayerInventory::default();
                inventory.main[0] = (*item != 0).then(|| ItemStack::one(Item(*item)));
                remote.equipment[0] = inventory.main[0];
                remote.player = Some(Box::new(RemotePlayer {
                    name: name.clone(),
                    entity: model,
                    inventory,
                    state: Default::default(),
                }));
            }
            Packet::Sleep {
                entity,
                status: 0,
                x,
                y,
                z,
            } => {
                let Some(remote) = self
                    .0
                    .get_mut(entity)
                    .and_then(|entity| entity.player.as_mut())
                else {
                    return false;
                };
                player::lie_down(&mut remote.entity, world, [*x, i32::from(*y), *z]);
            }
            Packet::Animation { entity, animation } => {
                let Some(remote) = self.0.get_mut(entity) else {
                    return false;
                };
                match *animation {
                    1 => {
                        if let Some(player) = &mut remote.player {
                            player.state.swing();
                        }
                    }
                    2 => remote.animation.hurt = 10,
                    3 => {
                        if let Some(player) = &mut remote.player {
                            player::get_up(&mut player.entity, world, 0.0, false);
                        }
                    }
                    _ => {}
                }
            }
            Packet::AttachEntity { entity, vehicle } => {
                let mount = self.0.contains_key(vehicle).then_some(*vehicle);
                let Some(entity) = self.0.get_mut(entity) else {
                    return false;
                };
                entity.riding = mount;
            }
            Packet::PickupSpawn {
                entity,
                item,
                x,
                y,
                z,
                rotation,
                pitch,
                roll,
            } => {
                let entity = self.spawn(*entity, Kind::Item(*item), [*x, *y, *z]);
                entity.remote = true;
                entity.velocity =
                    DVec3::new(f64::from(*rotation), f64::from(*pitch), f64::from(*roll)) / 128.0;
            }
            Packet::VehicleSpawn {
                entity,
                kind: kind @ (50 | 60 | 61 | 62 | 70 | 71),
                x,
                y,
                z,
                thrower,
                velocity,
            } => {
                let kind = match kind {
                    50 => Kind::Tnt,
                    60 => Kind::Arrow,
                    61 => Kind::Thrown(Item::SNOWBALL),
                    62 => Kind::Thrown(Item::EGG),
                    70 => Kind::Falling(Block::SAND),
                    _ => Kind::Falling(Block::GRAVEL),
                };
                let entity = self.spawn(*entity, kind, [*x, *y, *z]);
                // Moved by the client between the server's updates
                entity.remote = true;
                if matches!(kind, Kind::Tnt) {
                    entity.animation.fuse = 80;
                    entity.previous_animation.fuse = 80;
                }
                if *thrower > 0 {
                    entity.velocity = self::velocity(*velocity);
                }
            }
            Packet::VehicleSpawn {
                entity,
                kind: kind @ (1 | 10..=12),
                x,
                y,
                z,
                ..
            } => {
                let kind = match kind {
                    10 => VehicleKind::Minecart,
                    11 => VehicleKind::StorageCart,
                    12 => VehicleKind::PoweredCart,
                    _ => VehicleKind::Boat,
                };
                self.spawn(*entity, Kind::Vehicle(kind), [*x, *y, *z])
                    .remote = true;
            }
            Packet::VehicleSpawn {
                entity,
                kind: 90,
                x,
                y,
                z,
                thrower,
                ..
            } => {
                self.spawn(
                    *entity,
                    Kind::Hook((*thrower > 0).then_some(*thrower)),
                    [*x, *y, *z],
                );
            }
            Packet::VehicleSpawn {
                entity,
                kind: 63,
                x,
                y,
                z,
                velocity,
                ..
            } => {
                let entity = self.spawn(*entity, Kind::Fireball, [*x, *y, *z]);
                entity.remote = true;
                entity.flags[0] |= flags::ON_FIRE;
                entity.acceleration = self::velocity(*velocity);
            }
            Packet::EntityVelocity { entity, velocity } => {
                if let Some(entity) = self.0.get_mut(entity) {
                    entity.velocity = self::velocity(*velocity);
                }
            }
            Packet::MobSpawn {
                entity,
                kind,
                x,
                y,
                z,
                yaw,
                pitch,
                metadata,
            } => {
                let Some(kind) = MobKind::from_id(*kind) else {
                    return false;
                };
                let entity = self.spawn(*entity, Kind::Mob(kind), [*x, *y, *z]);
                entity.face(*yaw, *pitch);
                entity.metadata(metadata);
            }
            Packet::EntityMetadata { entity, metadata } => {
                let Some(entity) = self.0.get_mut(entity) else {
                    return false;
                };
                entity.metadata(metadata);
            }
            Packet::EntityState {
                entity,
                pos,
                rotation,
                walk,
                hurt,
                death,
                fuse,
                special,
                vehicle,
            } => {
                if let Some(entity) = self.0.get_mut(entity) {
                    entity.remote = false;
                    entity.pending = Some((
                        DVec3::from_array(*pos),
                        Animation {
                            rotation: *rotation,
                            walk: *walk,
                            hurt: *hurt,
                            death: *death,
                            fuse: *fuse,
                            special: *special,
                            vehicle: *vehicle,
                        },
                    ));
                    entity.remaining = 0;
                }
            }
            Packet::EntityTeleport {
                entity,
                x,
                y,
                z,
                yaw,
                pitch,
            } => {
                if let Some(entity) = self.0.get_mut(entity) {
                    entity.server_position = [*x, *y, *z];
                    // `handleEntityTeleport` lifts absolute positions by 1/64
                    entity.target = DVec3::new(f64::from(*x), f64::from(*y), f64::from(*z)) / 32.0
                        + DVec3::Y / 64.0;
                    entity.target_rotation = [angle(*yaw), angle(*pitch)];
                    entity.remote = true;
                    entity.remaining = entity.interpolation_steps();
                }
            }
            Packet::PlayerInventory {
                entity,
                slot,
                item,
                damage,
            } => {
                if let Some(entity) = self.0.get_mut(entity)
                    && let Some(target) = entity.equipment.get_mut(*slot as usize)
                {
                    *target =
                        (*item >= 0).then(|| ItemStack::new(Item(*item), 1, i32::from(*damage)));
                    if let Some(player) = &mut entity.player {
                        if *slot == 0 {
                            player.inventory.main[0] = *target;
                        } else if (1..=4).contains(slot) {
                            player.inventory.armor[(*slot - 1) as usize] = *target;
                        }
                    }
                }
            }
            Packet::RelEntityMove { entity, delta }
            | Packet::RelEntityMoveLook { entity, delta, .. } => {
                if let Some(entity) = self.0.get_mut(entity) {
                    entity.remote = true;
                    for (axis, d) in entity.server_position.iter_mut().zip(delta) {
                        *axis += i32::from(*d);
                    }
                    entity.target = DVec3::from_array(entity.server_position.map(f64::from)) / 32.0;
                    entity.remaining = entity.interpolation_steps();
                }
                if let Packet::RelEntityMoveLook { yaw, pitch, .. } = packet {
                    self.look(*entity, *yaw, *pitch);
                }
            }
            Packet::EntityLook { entity, yaw, pitch } => self.look(*entity, *yaw, *pitch),
            Packet::EntityStatus {
                entity,
                status: status @ 6..=8,
            } if self
                .0
                .get(entity)
                .is_some_and(|e| matches!(e.kind, Kind::Mob(MobKind::Wolf))) =>
            {
                let entity = self.0.get_mut(entity).unwrap();
                if *status == 8 {
                    entity.shaking = true;
                    entity.animation.special.shake = 0.0;
                } else {
                    let mut random = JavaRandom::new(i64::from(entity.age));
                    for _ in 0..7 {
                        let velocity = drift(&mut random);
                        let pos = entity.position
                            + DVec3::new(
                                f64::from(random.next_float() * 1.6 - 0.8),
                                0.5 + f64::from(random.next_float() * 0.8),
                                f64::from(random.next_float() * 1.6 - 0.8),
                            );
                        let name = if *status == 7 { "heart" } else { "smoke" };
                        self.2.push((name, pos, velocity));
                    }
                }
            }
            Packet::EntityStatus { entity, status } => {
                let Some(entity) = self.0.get_mut(entity).filter(|_| matches!(*status, 2 | 3))
                else {
                    return false;
                };
                entity.remote = true;
                let (hurt, dead) = (*status == 2, *status == 3);
                if hurt {
                    entity.animation.walk[1] = 1.5;
                    entity.animation.hurt = 10;
                }
                if dead {
                    entity.animation.death = entity.animation.death.max(1);
                }
                match entity.kind {
                    Kind::Mob(kind) => {
                        if let Some(sound) = kind.sound(hurt, dead) {
                            self.1.push((sound, entity.position, kind.volume(), None));
                        }
                    }
                    Kind::Player => self.1.push(("random.hurt", entity.position, 1.0, None)),
                    _ => {}
                }
            }
            Packet::DestroyEntity { entity } => {
                // A body removed before its death pose ended still disappears in a puff
                if let Some(mut removed) = self.0.remove(entity)
                    && let Kind::Mob(kind) = removed.kind
                    && removed.remote
                    && (1..22).contains(&removed.animation.death)
                {
                    removed.death_puff(kind, &mut self.2);
                }
            }
            // `handleCollect`: it flies into the collector, with a pop
            Packet::Collect {
                collected,
                collector,
            } => {
                let Some(entity) = self.0.get_mut(collected) else {
                    return false;
                };
                entity.collected = Some((*collector, 0, entity.position));
                let pitch =
                    ((entity.rand.next_float() - entity.rand.next_float()) * 0.7 + 1.0) * 2.0;
                self.1
                    .push(("random.pop", entity.position, 0.2, Some(pitch)));
            }
            _ => return false,
        }
        true
    }

    fn look(&mut self, entity: i32, yaw: i8, pitch: i8) {
        if let Some(entity) = self.0.get_mut(&entity) {
            entity.remote = true;
            entity.target_rotation = [angle(yaw), angle(pitch)];
            entity.remaining = entity.interpolation_steps();
        }
    }

    fn spawn(&mut self, id: i32, kind: Kind, position: [i32; 3]) -> &mut RemoteEntity {
        self.0.insert(id, RemoteEntity::new(id, kind, position));
        self.0.get_mut(&id).unwrap()
    }

    /// `local` is the id and position of the local player.
    pub fn tick(&mut self, world: &mut ClientWorld, local: (i32, DVec3)) {
        let mut picked = Vec::new();
        for (&id, entity) in &self.0 {
            if let Some((collector, _, _)) = entity.collected {
                let target = if collector == local.0 {
                    Some(local.1)
                } else {
                    self.0.get(&collector).map(|e| e.position)
                };
                picked.push((id, target.unwrap_or(local.1) - DVec3::Y * 0.5));
            }
        }
        for (id, target) in picked {
            let entity = self.0.get_mut(&id).unwrap();
            let Some((_, age, start)) = &mut entity.collected else {
                continue;
            };
            *age += 1;
            entity.previous = entity.position;
            let progress = (*age as f64 / 3.0).powi(2);
            entity.position = *start + (target - *start) * progress;
            if *age >= 3 {
                self.0.remove(&id);
            }
        }
        for entity in self.0.values_mut().filter(|e| e.collected.is_none()) {
            entity.previous = entity.position;
            entity.previous_animation = entity.animation;
            if let Some((position, animation)) = entity.pending.take() {
                entity.position = position;
                entity.animation = animation;
            } else if entity.remaining > 0 {
                entity.position += (entity.target - entity.position) / f64::from(entity.remaining);
                let yaw_delta = (entity.target_rotation[0] - entity.animation.rotation[0] + 180.0)
                    .rem_euclid(360.0)
                    - 180.0;
                entity.animation.rotation[0] += yaw_delta / entity.remaining as f32;
                entity.animation.rotation[1] += (entity.target_rotation[1]
                    - entity.animation.rotation[1])
                    / entity.remaining as f32;
                entity.remaining -= 1;
            }
            if entity.remote && matches!(entity.kind, Kind::Fireball) {
                if entity.remaining == 0 {
                    entity.position += entity.velocity;
                }
                let in_water = in_water(world, entity.position);
                if in_water {
                    let bubble = (
                        "bubble",
                        entity.position - entity.velocity * 0.25,
                        entity.velocity,
                    );
                    self.2.extend([bubble; 4]);
                }
                let drag = if in_water { 0.8_f32 } else { 0.95_f32 };
                entity.velocity = (entity.velocity + entity.acceleration) * f64::from(drag);
                self.2
                    .push(("smoke", entity.position + DVec3::Y * 0.5, DVec3::ZERO));
            }
            entity.age = entity.age.wrapping_add(1);
            if entity.remote && entity.remaining == 0 {
                entity.simulate(world, &mut self.2);
            }
            // `EntityCreeper.onUpdate` of a multiplayer client: the fuse follows its state
            if entity.remote && matches!(entity.kind, Kind::Mob(MobKind::Creeper)) {
                let state = if entity.flags[1] == 1 { 1 } else { -1 };
                if state > 0 && entity.animation.fuse == 0 {
                    self.1
                        .push(("random.fuse", entity.position, 1.0, Some(0.5)));
                }
                entity.animation.fuse = (entity.animation.fuse + state).clamp(0, 30);
            }
            if entity.remote {
                entity.animate();
                if let Kind::Mob(kind) = entity.kind {
                    // `EntityLiving.onEntityUpdate`: now and then a mob makes its sound
                    if entity.animation.death == 0
                        && entity.rand.next_int(1000) < entity.living_sound
                    {
                        entity.living_sound = -80;
                        if let Some(sound) = entity.living_sound_name(kind) {
                            let wolf = kind == MobKind::Wolf;
                            let volume = if wolf { 0.4 } else { kind.volume() };
                            self.1.push((sound, entity.position, volume, None));
                        }
                    } else {
                        entity.living_sound += 1;
                    }
                    // The puff of a body that has finished dying (`deathTime > 20`)
                    if entity.animation.death == 22 {
                        entity.death_puff(kind, &mut self.2);
                    }
                }
                if matches!(entity.kind, Kind::Mob(MobKind::Wolf)) {
                    entity.wolf_update(world, &mut self.1, &mut self.2);
                }
                if let Some((state, random)) = &mut entity.squid {
                    let in_water = in_water(world, entity.position + DVec3::Y * 0.475);
                    let mut motion = entity.position - entity.previous;
                    state.advance(
                        random,
                        &mut motion,
                        &mut entity.animation.rotation[2],
                        in_water,
                        true,
                    );
                    entity.animation.special = state.pose;
                }
                if matches!(entity.kind, Kind::Mob(MobKind::Slime | MobKind::Chicken)) {
                    let mut probe = Entity::new(0, 0.0, 0.0, JavaRandom::new(0));
                    probe.bb = entity.bounds();
                    let grounded = !common::entity::EntityWorld::colliding_boxes(
                        &*world,
                        &probe,
                        &probe.bb.offset(DVec3::new(0.0, -0.01, 0.0)),
                    )
                    .is_empty();
                    let special = &mut entity.animation.special;
                    if matches!(entity.kind, Kind::Mob(MobKind::Chicken)) {
                        special.flap.advance(grounded);
                    } else {
                        if grounded && !entity.grounded {
                            special.stretch = -0.5;
                        }
                        if !grounded && entity.grounded && entity.position.y > entity.previous.y {
                            special.stretch = 1.0;
                        }
                        special.stretch *= 0.6;
                    }
                    entity.grounded = grounded;
                }
            }
            let moving = entity.position != entity.previous;
            if matches!(entity.kind, Kind::Arrow) && entity.moving && !moving {
                entity.shake = 7;
            } else if entity.shake > 0 {
                entity.shake -= 1;
            }
            entity.moving = moving;
            if let Some(player) = &mut entity.player {
                let model = &mut player.entity;
                model.prev_pos = model.pos;
                model.prev_yaw = model.yaw;
                model.prev_pitch = model.pitch;
                if !model.sleep.sleeping {
                    model.set_position(entity.position);
                }
                model.yaw = entity.animation.rotation[0];
                model.pitch = entity.animation.rotation[1];
                model.flags = entity.flags[0];
                model.set_flag(flags::RIDING, entity.riding.is_some());
                model.ticks_existed = entity.age as i32;
                if let Some(living) = &mut model.living {
                    living.prev_walk_animation_speed = entity.previous_animation.walk[1];
                    living.walk_animation_speed = entity.animation.walk[1];
                    living.walk_animation_progress = entity.animation.walk[0];
                    living.prev_render_yaw_offset = entity.previous_animation.rotation[2];
                    living.render_yaw_offset = entity.animation.rotation[2];
                    living.hurt_time = entity.animation.hurt;
                    living.death_time = entity.animation.death;
                    living.prev_swing_progress = living.swing_progress;
                    living.swing_progress = player.state.tick_swing();
                }
            }
        }
        // `updateRidden`: riders sit on their mount once it moved, whatever their own packets
        let seats: Vec<_> = self
            .0
            .iter()
            .filter_map(|(&id, entity)| {
                let mount = self.mount(entity.riding?)?;
                let offset = if matches!(entity.kind, Kind::Player) {
                    -0.5
                } else {
                    0.0
                };
                Some((id, mount.seat + DVec3::Y * offset))
            })
            .collect();
        for (id, seat) in seats {
            if let Some(entity) = self.0.get_mut(&id) {
                entity.position = seat;
                if let Some(player) = &mut entity.player
                    && !player.entity.sleep.sleeping
                {
                    player.entity.set_position(seat);
                }
            }
        }
    }

    /// The seat and turn of a vehicle for its rider, or `None` if it is gone.
    pub fn mount(&self, id: i32) -> Option<crate::player::Mount> {
        let entity = self.0.get(&id)?;
        let offset = match entity.kind {
            Kind::Mob(kind) => kind.mounted_y_offset(),
            _ => 0.0,
        };
        let [yaw, pitch, _] = entity.animation.rotation;
        let [previous_yaw, previous_pitch, _] = entity.previous_animation.rotation;
        Some(crate::player::Mount {
            seat: if let Kind::Vehicle(kind) = entity.kind {
                kind.seat(entity.position, yaw)
            } else {
                entity.position + DVec3::Y * offset
            },
            turn: [yaw - previous_yaw, pitch - previous_pitch],
        })
    }

    /// `canBePushed` entities within `area`: living ones and boats, with their positions.
    pub fn pushers(&self, area: &Aabb) -> Vec<(i32, DVec3)> {
        self.0
            .iter()
            .filter(|(_, e)| {
                matches!(
                    e.kind,
                    Kind::Mob(_) | Kind::Player | Kind::Vehicle(VehicleKind::Boat)
                ) && e.animation.death == 0
                    && e.bounds().intersects(area)
            })
            .map(|(&id, e)| (id, e.position))
            .collect()
    }

    /// Whether a `preventEntitySpawning` entity stands in any of `boxes`.
    pub fn obstruct(&self, boxes: &[Aabb]) -> bool {
        self.0.values().any(|e| {
            matches!(
                e.kind,
                Kind::Mob(_) | Kind::Player | Kind::Vehicle(_) | Kind::Falling(_) | Kind::Tnt
            ) && e.animation.death == 0
                && boxes.iter().any(|b| b.intersects(&e.bounds()))
        })
    }

    /// The top of an entity, where a dismounting rider is put.
    pub fn top(&self, id: i32) -> Option<DVec3> {
        let entity = self.0.get(&id)?;
        Some(entity.position + DVec3::Y * (entity.bounds().max.y - entity.position.y))
    }

    pub fn target(&self, eye: DVec3, direction: DVec3, block_distance: f64) -> Option<i32> {
        let end = eye + direction * block_distance.min(3.0);
        self.0
            .iter()
            .filter(|(_, e)| {
                matches!(
                    e.kind,
                    Kind::Mob(_)
                        | Kind::Player
                        | Kind::Painting(_)
                        | Kind::Fireball
                        | Kind::Vehicle(_)
                        | Kind::Falling(_)
                        | Kind::Tnt
                ) && e.animation.death == 0
            })
            .filter_map(|(&id, e)| {
                // `getCollisionBorderSize`: fireballs are easy to hit back
                let fireball = matches!(e.kind, Kind::Fireball);
                let border = if fireball { 1.0 } else { f64::from(0.1_f32) };
                let bounds = e.bounds().inflate(DVec3::splat(border));
                if bounds.contains(eye) {
                    Some((id, 0.0))
                } else {
                    bounds
                        .intercept(eye, end)
                        .map(|(point, _)| (id, point.distance_squared(eye)))
                }
            })
            .min_by(|a, b| a.1.total_cmp(&b.1))
            .map(|(id, _)| id)
    }
}