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 / app / session.rs

use std::collections::VecDeque;
use std::time::{Duration, Instant};

use common::JavaRandom;
use common::entity::vehicle::VehicleKind;
use common::protocol::{Connection, Packet};
use common::world::shape::BlockView;
use common::world::{Block, ChunkPos, Material};
use glam::DVec3;
use server::IntegratedServer;

use super::StartError;
use crate::assets::Assets;
use crate::entities::Kind;
use crate::environment;
use crate::interaction::{Interaction, Screen};
use crate::menu::Page;
use crate::player::LocalPlayer;
use crate::render::animated::{AnimatedTextures, Dials, TileUpdate};
use crate::render::{FrameParams, Renderer};
use crate::settings::RenderDistance;
use crate::timer::Timer;
use crate::world::ClientWorld;

/// Time per frame a local game spends applying packets; the rest wait for the next frame.
const RECEIVE_TIME: Duration = Duration::from_millis(3);

pub enum Backend {
    Local(IntegratedServer),
    Remote(super::remote::Remote),
}

impl Backend {
    pub fn local(&self) -> bool {
        matches!(self, Self::Local(_))
    }
    pub fn shutdown(&mut self) -> Result<(), server::save::SaveError> {
        match self {
            Self::Local(server) => server.shutdown(),
            Self::Remote(remote) => {
                remote.disconnect();
                Ok(())
            }
        }
    }
    pub fn set_activity(&self, activity: server::Activity) {
        if let Self::Local(server) = self {
            server.set_activity(activity);
        }
    }
    pub fn options(&self, options: &crate::settings::Options) {
        if let Self::Local(server) = self {
            server.set_options(options.view_area(), options.difficulty);
        }
    }
}

pub struct Session {
    pub connection: Connection,
    pub backend: Backend,
    pub world: ClientWorld,
    pub player: LocalPlayer,
    pub interaction: Interaction,
    entities: crate::entities::Entities,
    scene: crate::render::scene::Geometry,
    timer: Timer,
    animated: AnimatedTextures,
    dials: Dials,
    tile_updates: Vec<TileUpdate>,
    pub render_distance: RenderDistance,
    fog_brightness: [f32; 2],
    sky_darkness: u32,
    /// Received messages with their age in ticks, newest last.
    pub chat: Vec<(String, u32)>,
    /// `EntityRenderer.rendererUpdateCount`.
    ticks: i32,
    rain_sound_counter: i32,
    pub fancy: bool,
    /// `GuiIngame.recordPlaying` and the ticks it stays up.
    record: Option<(String, i32)>,
    pub logged_in: bool,
    /// The dimension entered since the last frame, which reloads the terrain.
    pub travelled: Option<i8>,
    /// `GameSettings.hideGUI` (F1).
    pub hide_gui: bool,
    /// `GameSettings.smoothCamera` (F8): mouse turns are eased through `MouseFilter`s.
    pub smooth_camera: bool,
    pub camera_turn: [f32; 2],
    camera_filters: [MouseFilter; 2],
    /// `GameSettings.showDebugInfo` (F3).
    pub show_debug: bool,
    debug: Debug,
    statistics: crate::stats::Shared,
    /// Local packets left over when [`RECEIVE_TIME`] ran out, applied first next frame.
    backlog: VecDeque<Packet>,
}

/// `Minecraft.debug`: frames and chunk updates, counted over each second.
struct Debug {
    since: std::time::Instant,
    frames: u32,
    updates: u32,
    summary: String,
}

/// `MouseFilter`: eases a stream of mouse movements.
#[derive(Default)]
struct MouseFilter {
    actual: f32,
    smoothed: f32,
    latency: f32,
}

impl MouseFilter {
    fn smooth(&mut self, movement: f32, smoother: f32) -> f32 {
        self.actual += movement;
        let mut movement = (self.actual - self.smoothed) * smoother;
        self.latency += (movement - self.latency) * 0.5;
        if movement > 0.0 && movement > self.latency || movement < 0.0 && movement < self.latency {
            movement = self.latency;
        }
        self.smoothed += movement;
        movement
    }
}

impl Session {
    pub fn new(
        backend: Backend,
        connection: Connection,
        seed: i64,
        assets: &Assets,
        options: &crate::settings::Options,
        statistics: crate::stats::Shared,
    ) -> Self {
        backend.options(options);
        let remote = !backend.local();
        let mut player = LocalPlayer::new(seed);
        player.view_bobbing = options.view_bobbing;
        Self {
            statistics: statistics.clone(),
            backend,
            connection,
            world: ClientWorld::new(
                seed,
                assets.grass_colors.to_rgb(),
                assets.foliage_colors.to_rgb(),
            ),
            player,
            interaction: Interaction::new(statistics, remote),
            entities: Default::default(),
            scene: Default::default(),
            timer: Timer::new(),
            animated: AnimatedTextures::new(JavaRandom::new(seed).next_long()),
            dials: Dials::new(&assets.dials, seed),
            tile_updates: Vec::new(),
            render_distance: options.render_distance,
            fog_brightness: [0.0; 2],
            sky_darkness: 0,
            chat: Vec::new(),
            record: None,
            ticks: 0,
            rain_sound_counter: 0,
            fancy: options.fancy,
            logged_in: false,
            travelled: None,
            hide_gui: false,
            smooth_camera: false,
            camera_turn: [0.0; 2],
            camera_filters: Default::default(),
            show_debug: false,
            debug: Debug {
                since: std::time::Instant::now(),
                frames: 0,
                updates: 0,
                summary: "0 fps, 0 chunk updates".into(),
            },
            backlog: VecDeque::new(),
        }
    }

    pub fn center(&self) -> ChunkPos {
        let pos = self.player.entity.pos;
        ChunkPos::containing(
            common::math::floor_f64(pos.x),
            common::math::floor_f64(pos.z),
        )
    }

    pub fn loading_progress(&self) -> f32 {
        if !self.player.started {
            return 0.0;
        }
        let center = self.center();
        let loaded = (-1..=1)
            .flat_map(|dx| (-1..=1).map(move |dz| center.offset(dx, dz)))
            .filter(|&pos| self.world.contains(pos))
            .count();
        loaded as f32 / 9.0
    }

    pub fn ready(&self) -> bool {
        self.loading_progress() == 1.0
    }

    /// `NetClientHandler`: applies what the server sent.
    pub fn receive(
        &mut self,
        renderer: &mut Renderer,
        audio: &crate::audio::Audio,
    ) -> Result<(), StartError> {
        let local = self.backend.local();
        let budget = if local {
            usize::MAX
        } else {
            common::protocol::PACKETS_PER_TICK
        };
        let (received, closed) = self.connection.receive_some(budget);
        let mut packets = std::mem::take(&mut self.backlog);
        packets.extend(received);
        if closed
            && !packets
                .iter()
                .any(|p| matches!(p, Packet::Disconnect { .. }))
        {
            packets.push_back(Packet::Disconnect {
                reason: "End of stream".into(),
            });
        }
        let deadline = Instant::now() + RECEIVE_TIME;
        while let Some(packet) = packets.pop_front() {
            // A burst of terrain waits for the next frame instead of stalling this one
            if local && Instant::now() >= deadline {
                packets.push_front(packet);
                self.backlog = packets;
                break;
            }
            if self.entities.receive(&packet, &self.world) {
                self.entity_effects(audio);
                continue;
            }
            // `handleTransaction`: a rejected click is acknowledged so the server accepts more
            if let Packet::Transaction {
                window,
                action,
                accepted: false,
            } = packet
                && (window == 0 || window == self.interaction.window)
            {
                self.connection.send(Packet::Transaction {
                    window,
                    action,
                    accepted: true,
                });
            }
            if self.interaction.receive(&packet) {
                continue;
            }
            match packet {
                Packet::Disconnect { reason } => return Err(StartError::Disconnected(reason)),
                Packet::Login {
                    version_or_entity,
                    seed,
                    dimension,
                    ..
                } => {
                    self.logged_in = true;
                    tracing::info!("joined world with seed {seed}");
                    self.world.reset(seed, dimension);
                    self.player.entity.id = version_or_entity;
                }
                Packet::Statistic { id, amount } => {
                    self.statistics.lock().unwrap().server(id, amount)
                }
                Packet::UpdateTime { time } => self.world.time = time,
                Packet::EntityVelocity { entity, velocity } if entity == self.player.entity.id => {
                    self.player.entity.motion = crate::entities::velocity(velocity)
                }
                Packet::Sound {
                    name,
                    pos,
                    volume,
                    pitch,
                } => audio.play(&name, DVec3::from_array(pos), volume, pitch),
                Packet::Particle {
                    name,
                    pos,
                    velocity,
                } => self.world.particles.effect(
                    &name,
                    DVec3::from_array(pos),
                    DVec3::from_array(velocity),
                ),
                Packet::Explosion {
                    x,
                    y,
                    z,
                    size,
                    blocks,
                } => {
                    let origin = DVec3::new(x, y, z);
                    // `Explosion.doExplosionB`; the integrated server sends its own effects
                    let remote = !self.backend.local();
                    let rand = &mut self.world.rand;
                    if remote {
                        let pitch = (1.0 + (rand.next_float() - rand.next_float()) * 0.2) * 0.7;
                        audio.play("random.explode", origin, 4.0, pitch);
                    }
                    for &[dx, dy, dz] in blocks.iter().rev() {
                        let block = [
                            x as i32 + i32::from(dx),
                            y as i32 + i32::from(dy),
                            z as i32 + i32::from(dz),
                        ];
                        if remote {
                            let rand = &mut self.world.rand;
                            let point = DVec3::from_array(
                                block.map(|v| f64::from(v as f32 + rand.next_float())),
                            );
                            let delta = point - origin;
                            let length = f64::from(delta.length() as f32);
                            let speed = 0.5 / (length / f64::from(size) + 0.1)
                                * f64::from(rand.next_float() * rand.next_float() + 0.3);
                            let velocity = delta / length * speed;
                            let particles = &mut self.world.particles;
                            particles.effect("explode", (point + origin) / 2.0, velocity);
                            particles.effect("smoke", point, velocity);
                        }
                        self.world
                            .set_block(block[0], block[1], block[2], Block::AIR, 0);
                    }
                }
                Packet::AuxEffect {
                    effect,
                    x,
                    y,
                    z,
                    data,
                } => self.aux_effect(audio, effect, [x, i32::from(y as u8), z], data),
                Packet::PlayNoteBlock {
                    x,
                    y,
                    z,
                    instrument,
                    pitch,
                } => {
                    let y = i32::from(y);
                    if self.world.block(x, y, z) == Block::NOTE_BLOCK {
                        let center = DVec3::new(f64::from(x), f64::from(y), f64::from(z)) + 0.5;
                        let name = ["harp", "bd", "snare", "hat", "bassattack"]
                            .get(usize::from(instrument))
                            .copied()
                            .unwrap_or("harp");
                        let pitch_scale = 2.0_f64.powf((f64::from(pitch) - 12.0) / 12.0) as f32;
                        audio.play(&format!("note.{name}"), center, 3.0, pitch_scale);
                        self.world.particles.effect(
                            "note",
                            center + DVec3::Y * 0.7,
                            DVec3::new(f64::from(pitch) / 24.0, 0.0, 0.0),
                        );
                    }
                }
                Packet::EntityStatus {
                    entity,
                    status: 2 | 3,
                }
                | Packet::Animation {
                    entity,
                    animation: 2,
                } if entity == self.player.entity.id => {
                    let remote = !self.backend.local();
                    if let Some(living) = &mut self.player.entity.living {
                        living.hurt_time = 10;
                        living.max_hurt_time = 10;
                        if remote {
                            living.attacked_at_yaw = 0.0;
                        }
                    }
                    if remote {
                        audio.play("random.hurt", self.player.entity.pos, 1.0, 1.0);
                    }
                }
                Packet::HurtDirection { entity, yaw } if entity == self.player.entity.id => {
                    if let Some(living) = &mut self.player.entity.living {
                        living.attacked_at_yaw = yaw;
                    }
                }
                // The flags the server keeps for this player, like burning
                Packet::EntityMetadata { entity, metadata } if entity == self.player.entity.id => {
                    for (index, value) in metadata {
                        if index == 0
                            && let common::protocol::MetadataValue::Byte(flags) = value
                        {
                            let sneaking = self.player.entity.flag(common::entity::flags::SNEAKING);
                            self.player.entity.flags = flags as u8;
                            self.player
                                .entity
                                .set_flag(common::entity::flags::SNEAKING, sneaking);
                        }
                    }
                }
                Packet::UpdateHealth { health } => {
                    if let Some(living) = &mut self.player.entity.living {
                        if i32::from(health) < living.health {
                            living.prev_health = living.health;
                            living.hurt_time = 10;
                            living.max_hurt_time = 10;
                            self.player.entity.hearts_life = living.hearts_halves_life;
                        } else if i32::from(health) > living.health {
                            // `EntityPlayerSP.setHealth`
                            self.player.entity.hearts_life = living.hearts_halves_life / 2;
                        }
                        living.health = i32::from(health);
                    }
                    if health <= 0 {
                        self.interaction.screen = Some(Screen::Death);
                    }
                }
                Packet::Respawn { dimension } => {
                    let living = self.player.entity.living.as_ref();
                    let alive = living.is_some_and(|l| l.health > 0);
                    // Local portal travel keeps the same player (`Minecraft.usePortal`)
                    let travel = self.backend.local() && alive;
                    if dimension != self.world.dimension() {
                        let seed = self.world.seed();
                        self.world.reset(seed, dimension);
                        self.entities.0.clear();
                        audio.record(None, DVec3::ZERO);
                        self.record = None;
                        renderer.clear_world();
                        self.travelled = Some(dimension);
                        if travel {
                            let pitch = self.player.entity.rand.next_float() * 0.4 + 0.8;
                            audio.play_fx("portal.travel", 1.0, pitch);
                        }
                    }
                    let entity = &mut self.player.entity;
                    entity.dead = false;
                    if travel {
                        entity.portal.time = 1.0;
                        entity.portal.cooldown = 10;
                    } else {
                        entity.living = Some(common::entity::Living::new(20));
                        entity.fire = 0;
                        entity.air = 300;
                        entity.portal = Default::default();
                    }
                    self.player.started = false;
                }
                Packet::SpawnPosition { x, y, z } => self.world.spawn = [x, y, z],
                Packet::Sleep {
                    entity,
                    status: 0,
                    x,
                    y,
                    z,
                } if entity == self.player.entity.id => {
                    let bed = [x, i32::from(y), z];
                    common::entity::player::lie_down(&mut self.player.entity, &self.world, bed);
                }
                Packet::Animation {
                    entity,
                    animation: 3,
                } if entity == self.player.entity.id => common::entity::player::get_up(
                    &mut self.player.entity,
                    &self.world,
                    1.62,
                    false,
                ),
                Packet::AttachEntity { entity, vehicle } if entity == self.player.entity.id => {
                    let top = self
                        .player
                        .entity
                        .riding
                        .and_then(|id| self.entities.top(id));
                    let vehicle = self.entities.0.contains_key(&vehicle).then_some(vehicle);
                    self.player.attach(vehicle, top);
                }
                Packet::GameState { state: 0 } => {
                    self.message("Your home bed was missing or obstructed".into())
                }
                Packet::GameState {
                    state: state @ (1 | 2),
                } => {
                    let weather = &mut self.world.weather;
                    weather.raining = state == 1;
                    weather.rain = if weather.raining { 1.0 } else { 0.0 };
                    weather.previous_rain = weather.rain;
                }
                Packet::WeatherState {
                    raining,
                    thundering,
                    rain,
                    thunder,
                } => {
                    let weather = &mut self.world.weather;
                    weather.raining = raining;
                    weather.thundering = thundering;
                    weather.rain = rain;
                    weather.previous_rain = rain;
                    weather.thunder = thunder;
                    weather.previous_thunder = thunder;
                }
                Packet::Weather {
                    lightning: true,
                    x,
                    y,
                    z,
                    ..
                } => {
                    let position = DVec3::new(f64::from(x), f64::from(y), f64::from(z)) / 32.0;
                    let rand = JavaRandom::new(self.world.rand.next_long());
                    let bolt = crate::weather::Bolt::new(position, rand);
                    self.world.bolts.push(bolt);
                }
                Packet::PlayerPositionLook {
                    x,
                    y,
                    z,
                    yaw,
                    pitch,
                    ..
                } => self.player.teleport(&self.connection, x, y, z, yaw, pitch),
                Packet::PreChunk { x, z, load } => {
                    let pos = ChunkPos::new(x, z);
                    self.world.pre_chunk(pos, load);
                    if !load {
                        renderer.column_unloaded(pos);
                    }
                }
                Packet::MapData { item, map, data } if item == common::item::Item::MAP.0 => {
                    self.world
                        .maps
                        .entry(map)
                        .or_insert_with(|| common::map::Map::new([0, 0], 0))
                        .receive(&data);
                }
                Packet::MapChunk {
                    x,
                    y,
                    z,
                    size,
                    data,
                } => {
                    for pos in self.world.set_region(x, i32::from(y), z, size, &data) {
                        renderer.column_loaded(pos, &self.world);
                    }
                }
                Packet::BlockChange {
                    x,
                    y,
                    z,
                    block,
                    metadata,
                } => {
                    if self
                        .interaction
                        .placed_sign
                        .as_ref()
                        .is_some_and(|sign| sign.position == [x, i32::from(y), z])
                        && !matches!(Block(block), Block::SIGN | Block::WALL_SIGN)
                    {
                        self.interaction.placed_sign = None;
                    }
                    self.world
                        .set_block(x, i32::from(y), z, Block(block), metadata);
                }
                Packet::MultiBlockChange {
                    chunk_x,
                    chunk_z,
                    changes,
                } => {
                    for (packed, block, metadata) in changes {
                        let packed = packed as u16;
                        let x = chunk_x * 16 + i32::from(packed >> 12);
                        let z = chunk_z * 16 + i32::from(packed >> 8 & 15);
                        self.world
                            .set_block(x, i32::from(packed & 255), z, Block(block), metadata);
                    }
                }
                Packet::TileEntity { x, y, z, data } => self.world.set_tile_entity(x, y, z, *data),
                Packet::UpdateSign { x, y, z, lines } => {
                    let sign = common::world::TileEntity::Sign(lines);
                    self.world.set_tile_entity(x, i32::from(y), z, sign)
                }
                Packet::Chat { message } => self.message(message),
                Packet::KeepAlive => {
                    self.connection.send(Packet::KeepAlive);
                }
                other => tracing::debug!("unhandled packet {other:?}"),
            }
        }
        Ok(())
    }

    /// `RenderGlobal.playAuxSFX`: sounds and particles the server names by number.
    fn aux_effect(
        &mut self,
        audio: &crate::audio::Audio,
        effect: i32,
        [x, y, z]: [i32; 3],
        data: i32,
    ) {
        let corner = DVec3::new(f64::from(x), f64::from(y), f64::from(z));
        let random = &mut self.world.rand;
        match effect {
            1000 => audio.play("random.click", corner, 1.0, 1.0),
            1001 => audio.play("random.click", corner, 1.0, 1.2),
            1002 => audio.play("random.bow", corner, 1.0, 1.2),
            1003 => {
                let name = if self.player.entity.rand.next_double() < 0.5 {
                    "random.door_open"
                } else {
                    "random.door_close"
                };
                audio.play(name, corner + 0.5, 1.0, random.next_float() * 0.1 + 0.9);
            }
            1004 => {
                let pitch = 2.6 + (random.next_float() - random.next_float()) * 0.8;
                let center = [x, y, z].map(|v| f64::from(v as f32 + 0.5));
                audio.play("random.fizz", DVec3::from_array(center), 0.5, pitch);
            }
            1005 => {
                let name = common::item::Item(data as i16).record_name();
                if let Some(name) = name {
                    self.record = Some((format!("Now playing: C418 - {name}"), 60));
                }
                audio.record(name, corner);
            }
            2000 => {
                let (dx, dz) = (data % 3 - 1, data / 3 % 3 - 1);
                let origin = DVec3::new(
                    f64::from(x) + f64::from(dx) * 0.6 + 0.5,
                    f64::from(y) + 0.5,
                    f64::from(z) + f64::from(dz) * 0.6 + 0.5,
                );
                for _ in 0..10 {
                    let speed = random.next_double() * 0.2 + 0.01;
                    let position = DVec3::new(
                        origin.x
                            + f64::from(dx) * 0.01
                            + (random.next_double() - 0.5) * f64::from(dz) * 0.5,
                        origin.y + (random.next_double() - 0.5) * 0.5,
                        origin.z
                            + f64::from(dz) * 0.01
                            + (random.next_double() - 0.5) * f64::from(dx) * 0.5,
                    );
                    let velocity = DVec3::new(
                        f64::from(dx) * speed + random.next_gaussian() * 0.01,
                        -0.03 + random.next_gaussian() * 0.01,
                        f64::from(dz) * speed + random.next_gaussian() * 0.01,
                    );
                    self.world.particles.effect("smoke", position, velocity);
                }
            }
            2001 => self
                .world
                .break_effect([x, y, z], Block(data as u8), (data >> 8) as u8),
            _ => {}
        }
    }

    pub fn message(&mut self, message: String) {
        self.chat.push((message, 0));
        if self.chat.len() > 50 {
            self.chat.remove(0);
        }
    }

    /// Sounds and particles of remote entities; the integrated server makes its own.
    fn entity_effects(&mut self, audio: &crate::audio::Audio) {
        let sounds = std::mem::take(&mut self.entities.1);
        let effects = std::mem::take(&mut self.entities.2);
        if self.backend.local() {
            return;
        }
        let rand = &mut self.world.rand;
        for (sound, position, volume, pitch) in sounds {
            let pitch =
                pitch.unwrap_or_else(|| (rand.next_float() - rand.next_float()) * 0.2 + 1.0);
            audio.play(sound, position, volume, pitch);
        }
        for (name, position, velocity) in effects {
            self.world.particles.effect(name, position, velocity);
        }
    }

    /// One 20 Hz game tick, mirroring the parts of `Minecraft.runTick` the client has so far.
    fn tick(&mut self, audio: &crate::audio::Audio) {
        audio.music();
        // `EntityPlayerSP.onLivingUpdate`: the inventory hint stays until it is opened
        {
            let mut stats = self.statistics.lock().unwrap();
            if !stats.unlocked(0) {
                stats.hint(0);
            }
        }
        for (_, age) in &mut self.chat {
            *age += 1;
        }
        self.world.time += 1;
        self.ticks = self.ticks.wrapping_add(1);
        crate::weather::rain_particles(
            &mut self.world,
            self.player.entity.pos,
            self.ticks,
            self.fancy,
            &mut self.rain_sound_counter,
        );
        if let Some((_, ticks)) = &mut self.record {
            *ticks -= 1;
            if *ticks <= 0 {
                self.record = None;
            }
        }
        self.animated.tick(&mut self.tile_updates);
        let e = &self.player.entity;
        let [spawn_x, _, spawn_z] = self.world.spawn.map(f64::from);
        let targets = self.player.started.then(|| {
            [
                f64::from(e.yaw - 90.0) * std::f64::consts::PI / 180.0
                    - (spawn_z - e.pos.z).atan2(spawn_x - e.pos.x),
                f64::from(-self.world.celestial_angle(1.0) * std::f32::consts::PI * 2.0),
            ]
        });
        self.dials.tick(
            targets,
            self.world.dimension() == -1,
            &mut self.tile_updates,
        );
        let world = &mut self.world;
        world.lightning_flash = (world.lightning_flash - 1).max(0);
        world
            .bolts
            .retain_mut(|bolt| bolt.tick(&mut world.sounds, &mut world.lightning_flash));
        if world.has_sky() {
            world.weather.fade();
        }
        let weather = &self.world.weather;
        self.sky_darkness = environment::sky_darkness(
            self.world.celestial_angle(1.0),
            weather.rain_strength(1.0),
            weather.thunder_strength(1.0),
        );
        if self.player.entity.portal.inside && self.interaction.screen.is_some() {
            self.interaction.close(&self.connection);
        }
        let previous_position = self.player.entity.pos;
        let previous_ground = self.player.entity.on_ground;
        let previous_fall = self.player.entity.fall_distance;
        let riding = self.player.entity.riding;
        if riding.is_none() {
            let area = self.player.entity.bb.inflate(DVec3::new(0.2, 0.0, 0.2));
            let pushers = self.entities.pushers(&area);
            self.player
                .tick(&mut self.world, &self.connection, &pushers);
        }
        let local = (self.player.entity.id, self.player.entity.pos);
        self.entities.tick(&mut self.world, local);
        self.entity_effects(audio);
        self.world.vehicles.clear();
        self.world.vehicles.extend(
            self.entities
                .0
                .iter()
                .filter(|(_, entity)| matches!(entity.kind, Kind::Vehicle(VehicleKind::Boat)))
                .map(|(&id, entity)| (id, entity.bounds())),
        );
        if let Some(mount) = riding {
            let mount = self.entities.mount(mount);
            self.player
                .tick_ridden(&mut self.world, &self.connection, mount);
        }
        self.movement_stats(previous_position, previous_ground, previous_fall);
        self.interaction.tick(
            &mut self.world,
            &mut self.player,
            &self.entities,
            &self.connection,
        );
        let mut particles = std::mem::take(&mut self.world.particles);
        particles.viewer = self.player.entity.pos;
        particles.tick(&self.world);
        particles.ambient(&mut self.world, self.player.entity.pos);
        particles.spawners(&self.world, self.player.entity.feet());
        self.world.tick_spawners(self.player.entity.feet());
        self.world.particles = particles;
        self.world.tick_pistons(&mut self.player.entity);
        self.world.update_light();
        self.world.repack_blocks();
        for (name, position, volume, pitch) in self.world.sounds.drain(..) {
            audio.play(&name, position, volume, pitch);
        }
        for (name, volume, pitch) in self.world.sound_effects.drain(..) {
            audio.play_fx(name, volume, pitch);
        }

        let eye = self.player.eye(1.0);
        let light = self
            .world
            .light_level(eye.x, eye.y, eye.z, self.sky_darkness);
        let brightness = self.world.brightness[usize::from(light)];
        let distance_factor = self.render_distance.fog_factor();
        let target = brightness * (1.0 - distance_factor) + distance_factor;
        self.fog_brightness[1] = self.fog_brightness[0];
        self.fog_brightness[0] += (target - self.fog_brightness[0]) * 0.1;
    }

    /// `SoundManager.setListener`: the interpolated eye and yaw.
    pub fn listener(&self) -> (DVec3, f32) {
        let partial = self.timer.partial_ticks();
        let e = &self.player.entity;
        (
            self.player.eye(partial),
            e.prev_yaw + (e.yaw - e.prev_yaw) * partial,
        )
    }

    /// Runs the game ticks that are due, whether or not a frame gets drawn. Local games pause
    /// behind menus; remote ones carry on like the original.
    pub fn advance(&mut self, now: Instant, audio: &crate::audio::Audio, menu: &crate::menu::Menu) {
        self.tile_updates.clear();
        let ticks = if self.backend.local() && menu.page.is_some() {
            self.timer.freeze();
            // `Minecraft.preloadWorld` lights the terrain before play begins
            if menu.page == Some(Page::Loading) {
                self.world.update_light();
            }
            0
        } else {
            self.timer.advance(now)
        };
        for _ in 0..ticks {
            self.tick(audio);
        }
    }

    pub fn frame(
        &mut self,
        view: &mut super::View,
        menu: &crate::menu::Menu,
        options: &crate::settings::Options,
    ) -> Result<(), StartError> {
        let partial = self.timer.partial_ticks();
        if self.player.entity.is_inside_opaque_block(&self.world) {
            self.player.third_person = false;
        }
        self.interaction
            .refresh_shown(&self.world, &self.player, &self.entities, partial);
        if self.smooth_camera {
            let smoother = 0.05 * (options.sensitivity * 0.6 + 0.2).powi(3) * 8.0;
            let [dx, dy] = std::mem::take(&mut self.camera_turn);
            let [x, y] = &mut self.camera_filters;
            let turn = (x.smooth(dx, smoother), y.smooth(dy, smoother));
            self.player.entity.turn(turn.0, turn.1);
        }
        self.world.update_light();
        for [x, y, z] in self.world.take_dirty() {
            let pos = ChunkPos::new(x, z);
            if self.world.contains(pos) {
                view.renderer.section_changed(pos, y);
            }
        }

        let angle = self.world.celestial_angle(partial);
        let eye = self.player.eye(partial);
        let temperature = self.world.climate().raw_temperature(
            common::math::floor_f64(eye.x),
            common::math::floor_f64(eye.z),
        );
        let weather = &self.world.weather;
        let sky_color = crate::weather::sky_color(
            environment::sky_color(angle, temperature),
            weather,
            self.world.lightning_flash,
            partial,
        );
        let blend = 1.0 - (1.0 + 3.0 * self.render_distance.fog_factor()).powf(-0.25);
        let mut fog_color = if self.world.has_sky() {
            environment::fog_color(angle)
        } else {
            glam::Vec3::new(0.2, 0.03, 0.03)
        };
        fog_color += (sky_color - fog_color) * blend;
        fog_color = crate::weather::fog_color(fog_color, weather, partial);
        let submerged = [Material::Water, Material::Lava]
            .into_iter()
            .find(|&material| self.player.entity.eyes_in(&self.world, material));
        let fog_density = match submerged {
            Some(Material::Water) => {
                fog_color = glam::Vec3::new(0.02, 0.02, 0.2);
                Some(0.1)
            }
            Some(_) => {
                fog_color = glam::Vec3::new(0.6, 0.1, 0.0);
                Some(2.0)
            }
            None => None,
        };
        let [current, previous_brightness] = self.fog_brightness;
        fog_color *= previous_brightness + (current - previous_brightness) * partial;

        let extent = view.window.inner_size();
        let brightness = self.fancy.then(|| {
            let entity = &self.player.entity;
            let feet = entity.pos.y - f64::from(entity.y_offset);
            let y = feet + (entity.bb.max.y - entity.bb.min.y) * 0.66;
            let light = self
                .world
                .light_level(entity.pos.x, y, entity.pos.z, self.sky_darkness);
            self.world.brightness[usize::from(light)]
        });
        view.gui.build(
            &self.interaction,
            &self.player,
            [extent.width, extent.height],
            options.gui_scale,
            crate::gui::Overlay {
                brightness,
                partial,
                underwater: self.player.entity.eyes_in(&self.world, Material::Water),
            },
        );
        if let Some((message, ticks)) = &self.record {
            view.gui.record(message, *ticks as f32 - partial);
        }
        let typing = matches!(menu.page, Some(Page::Chat | Page::Sleep));
        view.gui.chat(&self.chat, typing);
        let hidden = self.hide_gui && self.interaction.screen.is_none() && menu.page.is_none();
        if hidden || menu.page == Some(Page::Loading) {
            view.gui
                .begin([extent.width, extent.height], options.gui_scale);
        }
        self.scene.hide_hand = self.hide_gui;
        self.scene.fancy = self.fancy;
        let (rendered, sections, updates) = view.renderer.debug_counts();
        let debug = &mut self.debug;
        debug.frames += 1;
        debug.updates += updates;
        if debug.since.elapsed() >= std::time::Duration::from_secs(1) {
            debug.summary = format!("{} fps, {} chunk updates", debug.frames, debug.updates);
            debug.since += std::time::Duration::from_secs(1);
            debug.frames = 0;
            debug.updates = 0;
        }
        if self.show_debug && !hidden {
            let entities = self.entities.0.len();
            let pos = self.player.entity.pos;
            let facing =
                common::math::floor_f64(f64::from(self.player.entity.yaw * 4.0 / 360.0) + 0.5) & 3;
            let chunks = if self.backend.local() {
                "ChunkCache: 1024".to_owned()
            } else {
                format!("MultiplayerChunkCache: {}", self.world.column_count())
            };
            view.gui.debug(
                &[
                    format!("Minecraft Beta 1.7.3 ({})", self.debug.summary),
                    format!("C: {rendered}/{sections}. F: 0, O: 0, E: 0"),
                    format!(
                        "P: {}. T: All: {}",
                        self.world.particles.particles.len(),
                        entities + 1
                    ),
                    format!("E: {entities}/{entities}. B: 0, I: 0"),
                    chunks,
                ],
                &[
                    format!("x: {:?}", pos.x),
                    format!("y: {:?}", pos.y),
                    format!("z: {:?}", pos.z),
                    format!("f: {facing}"),
                ],
            );
        }
        if !hidden {
            view.gui
                .achievement_toast(&self.statistics.lock().unwrap(), options);
        }
        let cursor = view.gui_space(options.gui_scale).1;
        view.gui.menu(menu, options, cursor);
        self.scene.build(
            &self.entities,
            &self.world,
            &self.player,
            &self.interaction,
            partial,
            self.sky_darkness,
        );
        self.scene
            .signs(&self.world, eye, &view.gui, self.sky_darkness);
        self.scene.map_label(&view.gui);
        let scene = &mut self.scene;
        scene.names(
            &self.entities,
            &self.player,
            &view.gui,
            partial,
            self.hide_gui,
        );
        self.scene.moving_blocks(
            &self.world,
            eye,
            partial,
            self.sky_darkness,
            crate::render::scene::Style {
                fancy: self.fancy,
                smooth_lighting: options.smooth_lighting,
            },
        );
        let viewer = crate::render::scene::Viewer {
            position: self.player.entity.pos,
            eye,
        };
        self.scene.precipitation(
            &self.world,
            &viewer,
            self.ticks,
            partial,
            self.fancy,
            self.sky_darkness,
        );
        self.scene.bolts(&self.world, eye);
        let entity = &self.player.entity;
        let camera = entity.prev_pos.lerp(entity.pos, f64::from(partial));
        if self.world.has_sky() {
            self.scene.clouds(
                DVec3::new(camera.x, eye.y, camera.z),
                self.ticks,
                partial,
                crate::weather::cloud_color(angle, &self.world.weather, partial),
                self.fancy,
            );
        }
        let inventory = &self.interaction.inventory;
        self.scene
            .build_portrait(&self.player, inventory, view.gui.portrait);
        view.renderer
            .render(
                Some(&self.world),
                &FrameParams {
                    eye,
                    view: self
                        .player
                        .view(&self.world, partial, options.view_bobbing, self.ticks),
                    fov_y_degrees: self.player.fov(&self.world, partial),
                    view_distance: self.render_distance.blocks(),
                    fog_color,
                    fog_density,
                    sky_color,
                    rain: self.world.weather.rain_strength(partial),
                    celestial_angle: angle,
                    sunrise: environment::sunrise_color(angle),
                    sky_darkness: self.sky_darkness,
                    has_sky: self.world.has_sky(),
                    brightness: self.world.brightness,
                    tile_updates: &self.tile_updates,
                    gui: &view.gui,
                    scene: &self.scene,
                },
            )
            .map_err(Into::into)
    }

    fn movement_stats(&mut self, previous: DVec3, grounded: bool, fall: f32) {
        if !self.player.started {
            return;
        }
        let entity = &self.player.entity;
        let delta = entity.pos - previous;
        let centimeters = |blocks: f32| (blocks * 100.0).round() as i32;
        let distance = centimeters(delta.length() as f32);
        let horizontal = centimeters(delta.with_y(0.0).length() as f32);
        let mut stats = self.statistics.lock().unwrap();
        stats.add(1100, 1);
        if let Some(mount) = entity.riding.and_then(|id| self.entities.0.get(&id)) {
            match mount.kind {
                Kind::Vehicle(VehicleKind::Boat) => stats.add(2007, distance),
                Kind::Vehicle(_) => stats.add(2006, distance),
                Kind::Mob(common::entity::mob::MobKind::Pig) => stats.add(2008, distance),
                _ => {}
            }
        } else {
            if entity.eyes_in(&self.world, Material::Water) {
                stats.add(2005, distance);
            } else if entity.in_water {
                stats.add(2001, horizontal);
            } else if entity.is_on_ladder(&self.world) {
                stats.add(2003, (delta.y * 100.0).round() as i32);
            } else if entity.on_ground {
                stats.add(2000, horizontal);
            } else if horizontal > 25 {
                stats.add(2004, horizontal);
            }
            if grounded && !entity.on_ground && self.player.keys.jump && entity.motion.y > 0.0 {
                stats.add(2010, 1);
            }
            if entity.on_ground && fall >= 2.0 {
                stats.add(2002, (f64::from(fall) * 100.0).round() as i32);
            }
        }
    }
}